WMM1: Might and Magic 1 like game
Browse files
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
---
|
| 2 |
-
title: WMM1
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
title: WMM1 魔法門傳說 太陽水晶
|
| 3 |
+
emoji: 🔮
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
向《魔法門 I》致敬的第一人稱網格迷宮 RPG。純靜態、所有圖形皆由程式以 12x12 像素精靈與透視多邊形繪製,風格一致。
|
game.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(()=>{
|
| 2 |
+
'use strict';
|
| 3 |
+
const $=id=>document.getElementById(id);
|
| 4 |
+
const cv=$('view'),cx=cv.getContext('2d');
|
| 5 |
+
const am=$('auto'),ac=am.getContext('2d');
|
| 6 |
+
const W0=320,H0=200,CX=160,CY=100,K=110,MAXF=6;
|
| 7 |
+
const DX=[0,1,0,-1],DY=[-1,0,1,0],DN=['北','東','南','西'];
|
| 8 |
+
const rnd=n=>Math.floor(Math.random()*n),rr=(a,b)=>a+rnd(b-a+1);
|
| 9 |
+
const SOLID='#wDXT123';
|
| 10 |
+
const isSolid=c=>SOLID.includes(c);
|
| 11 |
+
function mulberry(a){return()=>{a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
|
| 12 |
+
|
| 13 |
+
/* ---------- 像素精靈 (12x12) ---------- */
|
| 14 |
+
const BM={
|
| 15 |
+
blob:["............","............","....oooo....","...oooooo...","..oooooooo..","..owdoowdo..","..oooooooo..",".oooooooooo.",".oooddddooo.",".oooooooooo.","..dddddddd..","............"],
|
| 16 |
+
beast:["............","..o......o..",".ooo....ooo.",".oooooooooo.","..oorooroo..","..oooooooo..","...oowwoo...","...oooooo...","..oo.oo.oo..",".oo..oo..oo.","............","............"],
|
| 17 |
+
human:["....oooo....","...oooooo...","...owoowo...","...oooooo...","....oddo....","..cccccccc..",".o.cccccc.o.",".o.cccccc.o.","...cccccc...","...cc..cc...","...cc..cc...","..ddd..ddd.."],
|
| 18 |
+
skull:["...wwwwww...","..wwwwwwww..",".wwwwwwwwww.",".wddwwwwddw.",".wddwwwwddw.",".wwwwddwwww.","..wwwwwwww..","...wdwdwdw..","..cwwwwwwc..","..cwwwwwwc..","...w.cc.w...","...w....w..."],
|
| 19 |
+
ghost:["....oooo....","...oooooo...","..oooooooo..","..orooooro..","..orooooro..","..oooooooo..","..oooddooo..","..oooooooo..",".oooooooooo.",".oo.oooo.oo.",".o...oo...o.","............"],
|
| 20 |
+
bat:["............","o..........o","oo...oo...oo","ooo.oooo.ooo","oooooooooooo","oo.orooro.oo","o...oooo...o","....owwo....",".....oo.....","............","............","............"],
|
| 21 |
+
golem:["..oo....oo..","..ooo..ooo..","...oooooo...","..oorooroo..","..oooooooo..",".oooooooooo.","oo.oooooo.oo","oo.oooooo.oo","oo.oooooo.oo","...oo..oo...","..ooo..ooo..","..ddd..ddd.."],
|
| 22 |
+
lord:["..y.y.y.y...","..yyyyyyyy..","...oooooo...","...orooro...","...oooooo...",".cccooooccc.","cccccccccccc","ccoccccccocc","cccccccccccc",".cccc..cccc.",".cccc..cccc.",".ddd....ddd."],
|
| 23 |
+
chest:["............","............","............","............","..yyyyyyyy..",".oooooooooo.",".oddddddddo.",".ooooyyoooo.",".oooooooooo.",".oooooooooo.","............","............"]
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
const MON={
|
| 27 |
+
rat:{n:'巨鼠',s:'beast',o:'#8a6a4a',c:'#5a3a2a',hp:8,a:[1,3],d:0,xp:4,g:3},
|
| 28 |
+
wolf:{n:'野狼',s:'beast',o:'#8a8a90',c:'#444',hp:14,a:[2,5],d:0,xp:7,g:4},
|
| 29 |
+
bandit:{n:'強盜',s:'human',o:'#d0a070',c:'#604030',hp:18,a:[3,6],d:1,xp:10,g:12},
|
| 30 |
+
bat:{n:'蝙蝠',s:'bat',o:'#5a3a6a',c:'#222',hp:6,a:[1,3],d:0,xp:3,g:0},
|
| 31 |
+
goblin:{n:'地精',s:'human',o:'#70b040',c:'#6a4a20',hp:14,a:[2,5],d:0,xp:8,g:8},
|
| 32 |
+
gobarch:{n:'地精弓手',s:'human',o:'#60a040',c:'#30602a',hp:12,a:[3,6],d:0,xp:9,g:9},
|
| 33 |
+
slime:{n:'史萊姆',s:'blob',o:'#40c070',c:'#000',hp:20,a:[1,4],d:2,xp:9,g:2},
|
| 34 |
+
gobguard:{n:'地精衛兵',s:'human',o:'#508030',c:'#808890',hp:30,a:[4,8],d:2,xp:20,g:25},
|
| 35 |
+
gobking:{n:'地精王',s:'lord',o:'#70b040',c:'#a03030',hp:120,a:[6,12],d:3,xp:150,g:200,boss:1},
|
| 36 |
+
skel:{n:'骷髏兵',s:'skull',o:'#fff',c:'#a0a0a0',hp:30,a:[5,10],d:2,xp:20,g:15,und:1},
|
| 37 |
+
zombie:{n:'殭屍',s:'human',o:'#7a9a6a',c:'#4a4a5a',hp:45,a:[6,11],d:1,xp:24,g:10,und:1},
|
| 38 |
+
ghoul:{n:'食屍鬼',s:'beast',o:'#9ab0a0',c:'#506050',hp:38,a:[7,12],d:3,xp:30,g:20,und:1},
|
| 39 |
+
wraith:{n:'幽魂',s:'ghost',o:'#90a0e0',c:'#000',hp:30,a:[6,11],d:4,xp:32,g:25,und:1},
|
| 40 |
+
bknight:{n:'骸骨騎士',s:'skull',o:'#d8d0b8',c:'#506080',hp:75,a:[9,15],d:5,xp:70,g:80,und:1},
|
| 41 |
+
ashlord:{n:'灰燼騎士',s:'lord',o:'#c0b8a8',c:'#503030',hp:240,a:[12,20],d:6,xp:400,g:400,boss:1,und:1},
|
| 42 |
+
imp:{n:'小惡魔',s:'bat',o:'#d04040',c:'#222',hp:50,a:[10,16],d:4,xp:45,g:30},
|
| 43 |
+
garg:{n:'石像鬼',s:'golem',o:'#808890',c:'#444',hp:85,a:[12,18],d:8,xp:60,g:35},
|
| 44 |
+
dmage:{n:'黑暗法師',s:'human',o:'#c0a0c0',c:'#402060',hp:55,a:[12,22],d:3,xp:65,g:50},
|
| 45 |
+
golem:{n:'魔像',s:'golem',o:'#a08060',c:'#604020',hp:115,a:[14,20],d:10,xp:80,g:40},
|
| 46 |
+
shadow:{n:'暗影守衛',s:'ghost',o:'#6040a0',c:'#000',hp:150,a:[15,24],d:8,xp:150,g:90},
|
| 47 |
+
vorlag:{n:'大法師弗拉格',s:'lord',o:'#d0b0e0',c:'#301050',hp:520,a:[18,30],d:10,xp:2000,g:1000,boss:1,aoe:1}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
/* ---------- 地圖 ---------- */
|
| 51 |
+
const WORLD=[
|
| 52 |
+
"####################",
|
| 53 |
+
"#........#.........#",
|
| 54 |
+
"#.1......#....2....#",
|
| 55 |
+
"#........#.........#",
|
| 56 |
+
"#........#####.#####",
|
| 57 |
+
"#..................#",
|
| 58 |
+
"#....wwww..........#",
|
| 59 |
+
"#....wwww....T.....#",
|
| 60 |
+
"#..................#",
|
| 61 |
+
"#.......##.##..###.#",
|
| 62 |
+
"#.......#...#..#...#",
|
| 63 |
+
"#.......##3##..#...#",
|
| 64 |
+
"#..................#",
|
| 65 |
+
"#....#.......ww....#",
|
| 66 |
+
"#....#.......ww....#",
|
| 67 |
+
"#..................#",
|
| 68 |
+
"#..................#",
|
| 69 |
+
"#..................#",
|
| 70 |
+
"#..................#",
|
| 71 |
+
"####################"];
|
| 72 |
+
const ZONES={
|
| 73 |
+
world:{name:'瓦恩原野',tier:0,theme:'out',mons:['rat','wolf','bandit','bat'],sky:['#6aa0e0','#b0d8f0'],fl:['#3f7a35','#5aa045'],wall:[40,120,50]},
|
| 74 |
+
cellar:{name:'鼠穴地窖',tier:1,theme:'dun',mons:['rat','goblin','gobarch','slime'],elite:['gobguard','gobguard'],boss:'gobking',sky:['#0c0c14','#1c1c28'],fl:['#1c1610','#3a2e20'],wall:[120,105,90]},
|
| 75 |
+
crypt:{name:'灰燼地穴',tier:2,theme:'dun',mons:['skel','zombie','ghoul','wraith'],elite:['bknight','skel'],boss:'ashlord',sky:['#080a12','#161a26'],fl:['#14161c','#2c3038'],wall:[85,100,115]},
|
| 76 |
+
tower:{name:'弗拉格之塔',tier:3,theme:'dun',mons:['imp','garg','dmage','golem'],elite:['shadow','dmage'],boss:'vorlag',sky:['#100818','#241236'],fl:['#1a1020','#382448'],wall:[115,80,140]}
|
| 77 |
+
};
|
| 78 |
+
const DUN={cellar:{W:15,H:15,seed:11},crypt:{W:19,H:19,seed:23},tower:{W:21,H:21,seed:37}};
|
| 79 |
+
const NOTES={
|
| 80 |
+
cellar:['牆上刻著:「地精王吞下了銀鑰匙,想用它打開灰燼地穴。」','一張發霉的字條:「別相信史萊姆,它們比看起來更餓。」','塗鴉:「往深處走,最遠的角落有王座。」'],
|
| 81 |
+
crypt:['碑文:「灰燼騎士守著餘燼印記,沒有它,弗拉格之塔不會開啟。」','骨堆旁的日記:「太陽水晶碎裂的那天,天空變成了灰色。」','碑文:「聖光能灼傷不死者。」'],
|
| 82 |
+
tower:['魔法卷軸:「碎片一旦重聚,太陽將再度升起。」','黑板:「大法師弗拉格正在最高層等候你們。」','殘破的日記:「他曾是科拉克的學徒,如今只剩野心。」']
|
| 83 |
+
};
|
| 84 |
+
const MAPS={};
|
| 85 |
+
function gridFromRows(rows){return rows.map(r=>r.split(''))}
|
| 86 |
+
MAPS.world={name:'world',w:20,h:WORLD[0].length,g:gridFromRows(WORLD),zone:'world'};
|
| 87 |
+
MAPS.world.h=WORLD.length;
|
| 88 |
+
function genMaze(name){
|
| 89 |
+
const {W,H,seed}=DUN[name],rng=mulberry(seed),Z=ZONES[name];
|
| 90 |
+
const g=[];for(let y=0;y<H;y++)g.push(Array(W).fill('#'));
|
| 91 |
+
const st=[[1,1]];g[1][1]='.';
|
| 92 |
+
while(st.length){
|
| 93 |
+
const [x,y]=st[st.length-1],nb=[];
|
| 94 |
+
for(let d=0;d<4;d++){const nx=x+DX[d]*2,ny=y+DY[d]*2;if(nx>0&&ny>0&&nx<W-1&&ny<H-1&&g[ny][nx]==='#')nb.push(d)}
|
| 95 |
+
if(!nb.length){st.pop();continue}
|
| 96 |
+
const d=nb[Math.floor(rng()*nb.length)];
|
| 97 |
+
g[y+DY[d]][x+DX[d]]='.';g[y+DY[d]*2][x+DX[d]*2]='.';st.push([x+DX[d]*2,y+DY[d]*2]);
|
| 98 |
+
}
|
| 99 |
+
for(let y=1;y<H-1;y++)for(let x=1;x<W-1;x++){
|
| 100 |
+
if(g[y][x]!=='#')continue;
|
| 101 |
+
const h=x%2===0&&y%2===1&&g[y][x-1]==='.'&&g[y][x+1]==='.';
|
| 102 |
+
const v=y%2===0&&x%2===1&&g[y-1][x]==='.'&&g[y+1][x]==='.';
|
| 103 |
+
if((h||v)&&rng()<.1)g[y][x]='.';
|
| 104 |
+
}
|
| 105 |
+
const dist=Array.from({length:H},()=>Array(W).fill(-1));dist[1][1]=0;
|
| 106 |
+
const q=[[1,1]];let far=[1,1];
|
| 107 |
+
for(let i=0;i<q.length;i++){const [x,y]=q[i];if(dist[y][x]>dist[far[1]][far[0]])far=[x,y];
|
| 108 |
+
for(let d=0;d<4;d++){const nx=x+DX[d],ny=y+DY[d];if(g[ny][nx]!=='#'&&dist[ny][nx]<0){dist[ny][nx]=dist[y][x]+1;q.push([nx,ny])}}}
|
| 109 |
+
const open=(x,y)=>[0,1,2,3].filter(d=>g[y+DY[d]][x+DX[d]]!=='#').length;
|
| 110 |
+
const dead=[];
|
| 111 |
+
for(let y=1;y<H-1;y++)for(let x=1;x<W-1;x++)if(g[y][x]==='.'&&open(x,y)===1&&!(x===1&&y===1)&&!(x===far[0]&&y===far[1]))dead.push([x,y]);
|
| 112 |
+
for(let i=dead.length-1;i>0;i--){const j=Math.floor(rng()*(i+1));[dead[i],dead[j]]=[dead[j],dead[i]]}
|
| 113 |
+
g[far[1]][far[0]]='B';
|
| 114 |
+
const kinds=['H','c','M','c','n','M','c','n','c','M'];
|
| 115 |
+
dead.forEach((p,i)=>{g[p[1]][p[0]]=kinds[i%kinds.length]});
|
| 116 |
+
for(let y=2;y<H-2;y++)for(let x=2;x<W-2;x++){
|
| 117 |
+
if(g[y][x]!=='.'||dist[y][x]<4)continue;
|
| 118 |
+
const hz=g[y][x-1]!=='#'&&g[y][x+1]!=='#'&&g[y-1][x]==='#'&&g[y+1][x]==='#';
|
| 119 |
+
const vt=g[y-1][x]!=='#'&&g[y+1][x]!=='#'&&g[y][x-1]==='#'&&g[y][x+1]==='#';
|
| 120 |
+
if((hz||vt)&&rng()<.06)g[y][x]='D';
|
| 121 |
+
}
|
| 122 |
+
g[1][0]='X';
|
| 123 |
+
return {name,w:W,h:H,g,zone:name,far};
|
| 124 |
+
}
|
| 125 |
+
for(const k in DUN)MAPS[k]=genMaze(k);
|
| 126 |
+
|
| 127 |
+
/* ---------- 職業 ---------- */
|
| 128 |
+
const CLS={
|
| 129 |
+
knight:{n:'騎士',hp:[36,9],sp:[6,1.5],atk:[6,1.8],spells:[{n:'強擊',c:4,lv:1,k:'power'}]},
|
| 130 |
+
cleric:{n:'牧師',hp:[26,6],sp:[10,3],atk:[4,1],spells:[{n:'治療',c:3,lv:1,k:'heal'},{n:'聖光',c:3,lv:1,k:'holy'},{n:'群體治療',c:8,lv:4,k:'mheal'}]},
|
| 131 |
+
sorc:{n:'法師',hp:[20,4],sp:[14,3.5],atk:[2,.6],spells:[{n:'火花',c:2,lv:1,k:'spark'},{n:'火球術',c:6,lv:3,k:'fire'}]},
|
| 132 |
+
archer:{n:'弓手',hp:[28,6.5],sp:[6,2],atk:[5,1.5],spells:[{n:'連射',c:3,lv:1,k:'volley'}]}
|
| 133 |
+
};
|
| 134 |
+
const WEAP=[{n:'鐵劍',b:3,p:60},{n:'鋼劍',b:6,p:200},{n:'秘銀劍',b:10,p:600}];
|
| 135 |
+
const ARM=[{n:'皮甲',b:1,p:40},{n:'鎖甲',b:3,p:150},{n:'板甲',b:6,p:450},{n:'秘銀甲',b:9,p:1000}];
|
| 136 |
+
const atkOf=p=>Math.floor(CLS[p.cls].atk[0]+CLS[p.cls].atk[1]*(p.lv-1));
|
| 137 |
+
const mspOf=p=>Math.floor(CLS[p.cls].sp[0]+CLS[p.cls].sp[1]*(p.lv-1));
|
| 138 |
+
function mkChar(name,cls){const c=CLS[cls];return{name,cls,lv:1,xp:0,hp:c.hp[0],mhp:c.hp[0],sp:c.sp[0],msp:c.sp[0],w:0,a:0,wn:'木棍',an:'布衣',dg:false}}
|
| 139 |
+
|
| 140 |
+
/* ---------- 狀態 ---------- */
|
| 141 |
+
let S=null,C=null,MENU=null;
|
| 142 |
+
function newState(){return{map:'world',x:13,y:9,d:0,gold:100,items:{potion:3,ether:1},keys:{},mod:{},vis:{},steps:0,cd:3,mode:'explore',ret:null,won:false,
|
| 143 |
+
party:[mkChar('雷歐','knight'),mkChar('莉娜','cleric'),mkChar('馬格','sorc'),mkChar('席拉','archer')]}}
|
| 144 |
+
const SAVE='wmm1_save_v1';
|
| 145 |
+
function save(){try{localStorage.setItem(SAVE,JSON.stringify(S))}catch(e){}}
|
| 146 |
+
function load(){try{const t=localStorage.getItem(SAVE);return t?JSON.parse(t):null}catch(e){return null}}
|
| 147 |
+
const cellAt=(m,x,y)=>{const M=MAPS[m];if(x<0||y<0||x>=M.w||y>=M.h)return'#';const o=S.mod[m+':'+x+','+y];return o!==undefined?o:M.g[y][x]};
|
| 148 |
+
const setCell=(x,y,c)=>{S.mod[S.map+':'+x+','+y]=c};
|
| 149 |
+
const zone=()=>ZONES[MAPS[S.map].zone];
|
| 150 |
+
const P=()=>S.party;
|
| 151 |
+
const alive=()=>P().filter(p=>p.hp>0);
|
| 152 |
+
|
| 153 |
+
/* ---------- 記錄 ---------- */
|
| 154 |
+
function say(t,c){const l=$('log'),d=document.createElement('div');if(c)d.className=c;d.textContent=t;l.appendChild(d);while(l.children.length>80)l.removeChild(l.firstChild);l.scrollTop=l.scrollHeight}
|
| 155 |
+
|
| 156 |
+
/* ---------- 選單 ---------- */
|
| 157 |
+
function showMenu(title,text,opts){
|
| 158 |
+
MENU=opts;$('ovTitle').textContent=title;$('ovText').textContent=text;
|
| 159 |
+
const o=$('ovOpts');o.innerHTML='';
|
| 160 |
+
opts.forEach((op,i)=>{const b=document.createElement('button');b.textContent=(i+1)+'. '+op.t;b.onclick=()=>pick(i);o.appendChild(b)});
|
| 161 |
+
$('overlay').hidden=false;
|
| 162 |
+
}
|
| 163 |
+
function hideMenu(){MENU=null;$('overlay').hidden=true}
|
| 164 |
+
function pick(i){const op=MENU&&MENU[i];if(!op)return;hideMenu();op.f&&op.f()}
|
| 165 |
+
const OK=f=>[{t:'確定',f}];
|
| 166 |
+
|
| 167 |
+
/* ---------- 指令列 ---------- */
|
| 168 |
+
function setCmd(list){const c=$('cmd');c.innerHTML='';list.forEach(([t,f,dis])=>{const b=document.createElement('button');b.textContent=t;b.onclick=f;if(dis)b.disabled=true;c.appendChild(b)})}
|
| 169 |
+
function cmdExplore(){setCmd([['↶ 左轉(A)',()=>turn(-1)],['▲ 前進(W)',()=>move(0)],['↷ 右轉(D)',()=>turn(1)],['◀ 左移(Q)',()=>move(3)],['▼ 後退(S)',()=>move(2)],['▶ 右移(E)',()=>move(1)],['物品(I)',itemMenu],['地圖(M)',()=>{am.style.display=am.style.display==='none'?'block':'none'}]])}
|
| 170 |
+
|
| 171 |
+
/* ---------- 繪圖:像素 ---------- */
|
| 172 |
+
function bitmap(bm,pal,x,y,px){
|
| 173 |
+
for(let r=0;r<12;r++)for(let c=0;c<12;c++){const ch=bm[r][c];if(ch==='.')continue;cx.fillStyle=pal[ch];cx.fillRect(Math.round(x+c*px),Math.round(y+r*px),Math.ceil(px),Math.ceil(px))}
|
| 174 |
+
}
|
| 175 |
+
const palOf=m=>({o:m.o,c:m.c,d:'#1a1020',w:'#f0f0e8',r:'#e03030',y:'#f0c030'});
|
| 176 |
+
const sh=(rgb,f)=>`rgb(${rgb.map(v=>Math.round(v*f)).join(',')})`;
|
| 177 |
+
const sx=(x,z)=>CX+x*K/z,sy=(y,z)=>CY+y*K/z;
|
| 178 |
+
const fade=z=>Math.max(.22,1-z/7.5);
|
| 179 |
+
|
| 180 |
+
function drawBg(z){
|
| 181 |
+
let g=cx.createLinearGradient(0,0,0,CY);g.addColorStop(0,z.sky[0]);g.addColorStop(1,z.sky[1]);cx.fillStyle=g;cx.fillRect(0,0,W0,CY);
|
| 182 |
+
g=cx.createLinearGradient(0,CY,0,H0);g.addColorStop(0,z.fl[0]);g.addColorStop(1,z.fl[1]);cx.fillStyle=g;cx.fillRect(0,CY,W0,H0-CY);
|
| 183 |
+
}
|
| 184 |
+
function baseColor(c,Z){
|
| 185 |
+
if(c==='w')return[50,100,200];
|
| 186 |
+
if(c==='D')return[130,85,40];
|
| 187 |
+
if(c==='X')return[200,190,150];
|
| 188 |
+
if(c==='T')return[190,160,110];
|
| 189 |
+
if(c==='1')return[110,90,70];
|
| 190 |
+
if(c==='2')return[80,85,100];
|
| 191 |
+
if(c==='3')return[110,70,140];
|
| 192 |
+
return Z.wall;
|
| 193 |
+
}
|
| 194 |
+
function front(f,l,c,Z){
|
| 195 |
+
const z=f-.5,x1=sx(l-.5,z),x2=sx(l+.5,z),y1=sy(-.5,z),y2=sy(.5,z),w=x2-x1,h=y2-y1,fd=fade(z);
|
| 196 |
+
cx.fillStyle=sh(baseColor(c,Z),fd);cx.fillRect(x1,y1,w,h);
|
| 197 |
+
cx.strokeStyle='rgba(0,0,0,.55)';cx.lineWidth=1;
|
| 198 |
+
if(c==='#'||c==='w'){
|
| 199 |
+
cx.beginPath();
|
| 200 |
+
for(let i=1;i<4;i++){const y=y1+h*i/4;cx.moveTo(x1,y);cx.lineTo(x2,y);
|
| 201 |
+
const off=(i%2)?w/3:w*2/3;cx.moveTo(x1+off,y);cx.lineTo(x1+off,y+h/4);}
|
| 202 |
+
cx.stroke();
|
| 203 |
+
if(Z.theme==='out'&&c==='#'){cx.fillStyle=sh([25,85,35],fd);for(let i=0;i<5;i++){cx.beginPath();cx.arc(x1+w*(.15+.18*i),y1+h*(.2+.12*(i%3)),w*.12,0,7);cx.fill()}}
|
| 204 |
+
}
|
| 205 |
+
if(c==='D'){cx.fillStyle=sh([70,45,20],fd);cx.fillRect(x1+w*.15,y1+h*.1,w*.7,h*.9);cx.fillStyle=sh([240,200,60],fd);cx.fillRect(x1+w*.68,y1+h*.55,w*.07,w*.07)}
|
| 206 |
+
if(c==='X'){cx.fillStyle=sh([30,30,40],fd);cx.fillRect(x1+w*.25,y1+h*.2,w*.5,h*.8);cx.fillStyle=sh([255,240,120],fd);cx.fillRect(x1+w*.4,y1+h*.3,w*.2,h*.1)}
|
| 207 |
+
if(c==='T'){cx.fillStyle=sh([170,50,40],fd);cx.beginPath();cx.moveTo(x1,y1+h*.3);cx.lineTo(x1+w/2,y1);cx.lineTo(x2,y1+h*.3);cx.fill();
|
| 208 |
+
cx.fillStyle=sh([60,35,20],fd);cx.fillRect(x1+w*.38,y1+h*.55,w*.24,h*.45);cx.fillStyle=sh([255,230,120],fd);cx.fillRect(x1+w*.12,y1+h*.45,w*.15,h*.15);cx.fillRect(x1+w*.73,y1+h*.45,w*.15,h*.15)}
|
| 209 |
+
if(c==='1'||c==='2'||c==='3'){cx.fillStyle=sh([10,5,15],fd);cx.fillRect(x1+w*.25,y1+h*.35,w*.5,h*.65);cx.beginPath();cx.arc(x1+w/2,y1+h*.35,w*.25,Math.PI,0);cx.fill();
|
| 210 |
+
if(c==='3'){cx.fillStyle=sh([150,110,190],fd);cx.fillRect(x1+w*.4,y1-h*.25,w*.2,h*.3)}}
|
| 211 |
+
cx.strokeRect(x1,y1,w,h);
|
| 212 |
+
}
|
| 213 |
+
function side(f,l,left,c,Z){
|
| 214 |
+
const xs=left?l-.5:l+.5,za=Math.max(.3,f-.5),zb=f+.5,fd=fade((za+zb)/2)*.78;
|
| 215 |
+
cx.fillStyle=sh(baseColor(c,Z),fd);
|
| 216 |
+
cx.beginPath();cx.moveTo(sx(xs,za),sy(-.5,za));cx.lineTo(sx(xs,zb),sy(-.5,zb));cx.lineTo(sx(xs,zb),sy(.5,zb));cx.lineTo(sx(xs,za),sy(.5,za));cx.closePath();cx.fill();
|
| 217 |
+
cx.strokeStyle='rgba(0,0,0,.55)';cx.stroke();
|
| 218 |
+
if(c==='#'||c==='w'){cx.beginPath();for(let i=1;i<4;i++){const t=i/4,ya=sy(-.5+t,za),yb=sy(-.5+t,zb);cx.moveTo(sx(xs,za),ya);cx.lineTo(sx(xs,zb),yb)}cx.stroke()}
|
| 219 |
+
if(c==='D'){cx.fillStyle=sh([70,45,20],fd);cx.beginPath();const z1=za+(zb-za)*.2,z2=zb-(zb-za)*.2;cx.moveTo(sx(xs,z1),sy(-.35,z1));cx.lineTo(sx(xs,z2),sy(-.35,z2));cx.lineTo(sx(xs,z2),sy(.5,z2));cx.lineTo(sx(xs,z1),sy(.5,z1));cx.fill()}
|
| 220 |
+
}
|
| 221 |
+
function sprite(f,l,c,Z){
|
| 222 |
+
const z=f,x=l,s=K/z;cx.globalAlpha=fade(z);
|
| 223 |
+
if(c==='c'){bitmap(BM.chest,{o:'#a06a2a',d:'#3a2010',y:'#f0c030'},sx(x,z)-s*.4,sy(.5,z)-s*.8,s*.8/12)}
|
| 224 |
+
else if(c==='M'||c==='B'){const id=c==='B'?Z.boss:Z.elite[0],m=MON[id],k=c==='B'?1.1:.8;bitmap(BM[m.s],palOf(m),sx(x,z)-s*k/2,sy(.5,z)-s*k,s*k/12)}
|
| 225 |
+
else if(c==='H'){cx.fillStyle='#9a9aa8';cx.fillRect(sx(x,z)-s*.3,sy(.5,z)-s*.35,s*.6,s*.35);cx.fillStyle='#40a0ff';cx.fillRect(sx(x,z)-s*.25,sy(.5,z)-s*.45,s*.5,s*.12)}
|
| 226 |
+
else if(c==='n'){cx.fillStyle='#6a4a20';cx.fillRect(sx(x,z)-s*.04,sy(.5,z)-s*.6,s*.08,s*.6);cx.fillStyle='#d8c090';cx.fillRect(sx(x,z)-s*.25,sy(.5,z)-s*.65,s*.5,s*.3)}
|
| 227 |
+
cx.globalAlpha=1;
|
| 228 |
+
}
|
| 229 |
+
function drawExplore(){
|
| 230 |
+
const Z=zone();drawBg(Z);
|
| 231 |
+
const r=(S.d+1)%4,at=(f,l)=>cellAt(S.map,S.x+DX[S.d]*f+DX[r]*l,S.y+DY[S.d]*f+DY[r]*l);
|
| 232 |
+
const ls=[];for(let l=-6;l<=6;l++)ls.push(l);ls.sort((a,b)=>Math.abs(b)-Math.abs(a));
|
| 233 |
+
for(let f=MAXF;f>=0;f--)for(const l of ls){
|
| 234 |
+
if(f===0&&l!==0)continue;
|
| 235 |
+
const c=at(f,l);
|
| 236 |
+
if(isSolid(c)){if(f>0)front(f,l,c,Z)}
|
| 237 |
+
else{
|
| 238 |
+
if(l<=0){const n=at(f,l-1);if(isSolid(n))side(f,l,true,n,Z)}
|
| 239 |
+
if(l>=0){const n=at(f,l+1);if(isSolid(n))side(f,l,false,n,Z)}
|
| 240 |
+
if(f>0&&'cMBHn'.includes(c))sprite(f,l,c,Z);
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
function drawCombat(){
|
| 245 |
+
const Z=zone();cx.fillStyle='#0a0612';cx.fillRect(0,0,W0,H0);
|
| 246 |
+
const g=cx.createLinearGradient(0,0,0,H0);g.addColorStop(0,'#120a20');g.addColorStop(1,Z.theme==='out'?'#1c3018':'#1c1410');cx.fillStyle=g;cx.fillRect(0,0,W0,H0);
|
| 247 |
+
const en=C.en,n=en.length,big=en.some(e=>e.boss);
|
| 248 |
+
const px=big?9:n>3?3.6:4.6,w=px*12;
|
| 249 |
+
en.forEach((e,i)=>{
|
| 250 |
+
const x=(W0/n)*(i+.5)-w/2,y=big?30:60;
|
| 251 |
+
cx.globalAlpha=e.hp>0?1:.15;bitmap(BM[e.s],palOf(e),x,y,px);cx.globalAlpha=1;
|
| 252 |
+
if(e.hp>0){cx.fillStyle='#300';cx.fillRect(x,y+w+4,w,4);cx.fillStyle='#e04040';cx.fillRect(x,y+w+4,w*e.hp/e.mhp,4);
|
| 253 |
+
cx.fillStyle='#f0e0b0';cx.font='10px monospace';cx.textAlign='center';cx.fillText(e.n,x+w/2,y+w+18);cx.textAlign='left'}
|
| 254 |
+
});
|
| 255 |
+
}
|
| 256 |
+
function drawTown(){
|
| 257 |
+
drawBg({sky:['#6aa0e0','#c8e4f8'],fl:['#6a8a40','#4a6a30']});
|
| 258 |
+
const bl=[[20,'旅店','#b06040'],[120,'神殿','#d0d0e0'],[220,'商店','#c09030']];
|
| 259 |
+
bl.forEach(([x,t,col])=>{cx.fillStyle=col;cx.fillRect(x,70,80,70);cx.fillStyle='#703020';cx.beginPath();cx.moveTo(x-6,70);cx.lineTo(x+40,40);cx.lineTo(x+86,70);cx.fill();
|
| 260 |
+
cx.fillStyle='#3a2010';cx.fillRect(x+32,105,16,35);cx.fillStyle='#ffe070';cx.fillRect(x+10,85,14,14);cx.fillRect(x+56,85,14,14);
|
| 261 |
+
cx.fillStyle='#fff';cx.font='12px monospace';cx.textAlign='center';cx.fillText(t,x+40,66);cx.textAlign='left'});
|
| 262 |
+
cx.fillStyle='#b8a070';cx.fillRect(0,140,W0,20);
|
| 263 |
+
}
|
| 264 |
+
function drawTitle(){
|
| 265 |
+
const g=cx.createLinearGradient(0,0,0,H0);g.addColorStop(0,'#1a0a30');g.addColorStop(1,'#502010');cx.fillStyle=g;cx.fillRect(0,0,W0,H0);
|
| 266 |
+
cx.fillStyle='#f0c030';cx.beginPath();cx.arc(160,120,34,0,7);cx.fill();cx.fillStyle='#fff6a0';cx.beginPath();cx.arc(160,120,22,0,7);cx.fill();
|
| 267 |
+
cx.fillStyle='#000';cx.fillRect(0,150,W0,50);
|
| 268 |
+
cx.fillStyle='#f0c030';cx.font='bold 22px monospace';cx.textAlign='center';cx.fillText('魔法門傳說',160,40);cx.font='14px monospace';cx.fillStyle='#e8dcc0';cx.fillText('太陽水晶 · WMM1',160,62);cx.textAlign='left';
|
| 269 |
+
}
|
| 270 |
+
function drawScene(){
|
| 271 |
+
if(!S){drawTitle();return}
|
| 272 |
+
if(S.mode==='combat'&&C)drawCombat();else if(S.mode==='town')drawTown();else drawExplore();
|
| 273 |
+
}
|
| 274 |
+
function drawAuto(){
|
| 275 |
+
ac.fillStyle='#050308';ac.fillRect(0,0,160,160);
|
| 276 |
+
if(!S||S.mode==='town')return;
|
| 277 |
+
const M=MAPS[S.map],cs=Math.floor(160/Math.max(M.w,M.h)),v=S.vis[S.map]||{};
|
| 278 |
+
for(let y=0;y<M.h;y++)for(let x=0;x<M.w;x++){
|
| 279 |
+
if(!v[x+','+y])continue;const c=cellAt(S.map,x,y);
|
| 280 |
+
ac.fillStyle=isSolid(c)?(c==='#'?'#4a4258':c==='w'?'#2a4a9a':'#c8a040'):'#1e1a28';
|
| 281 |
+
if(!isSolid(c)&&'cMBHn'.includes(c))ac.fillStyle='#d0a030';
|
| 282 |
+
ac.fillRect(x*cs,y*cs,cs,cs);
|
| 283 |
+
}
|
| 284 |
+
ac.fillStyle='#ff4040';ac.fillRect(S.x*cs,S.y*cs,cs,cs);
|
| 285 |
+
ac.fillStyle='#fff';ac.fillRect(S.x*cs+cs/2-1+DX[S.d]*cs/2,S.y*cs+cs/2-1+DY[S.d]*cs/2,2,2);
|
| 286 |
+
}
|
| 287 |
+
function markVis(){
|
| 288 |
+
const v=S.vis[S.map]||(S.vis[S.map]={});
|
| 289 |
+
for(let dy=-1;dy<=1;dy++)for(let dx=-1;dx<=1;dx++)v[(S.x+dx)+','+(S.y+dy)]=1;
|
| 290 |
+
const r=(S.d+1)%4;
|
| 291 |
+
for(let f=1;f<=MAXF;f++){const x=S.x+DX[S.d]*f,y=S.y+DY[S.d]*f,c=cellAt(S.map,x,y);v[x+','+y]=1;if(isSolid(c))break;
|
| 292 |
+
for(const l of[-1,1]){const a=x+DX[r]*l,b=y+DY[r]*l;v[a+','+b]=1}}
|
| 293 |
+
}
|
| 294 |
+
function renderParty(){
|
| 295 |
+
const el=$('party');el.innerHTML='';if(!S)return;
|
| 296 |
+
P().forEach((p,i)=>{
|
| 297 |
+
const d=document.createElement('div');d.className='pc'+(p.hp<=0?' dead':'')+(S.mode==='combat'&&C&&C.i===i?' act':'');
|
| 298 |
+
d.innerHTML=`${p.name} <small>${CLS[p.cls].n} Lv${p.lv}</small> ${p.hp<=0?'【死亡】':''}<br>HP ${Math.max(0,p.hp)}/${p.mhp} SP ${p.sp}/${p.msp}<div class="bar"><i style="width:${Math.max(0,p.hp)/p.mhp*100}%"></i></div><div class="bar sp"><i style="width:${p.msp?p.sp/p.msp*100:0}%"></i></div><div class="eq">${p.wn}(+${p.w}) ${p.an}(+${p.a}) XP ${p.xp}/${35*p.lv*p.lv}</div>`;
|
| 299 |
+
el.appendChild(d);
|
| 300 |
+
});
|
| 301 |
+
$('where').textContent=S.mode==='town'?'索皮加爾鎮 金幣 '+S.gold:`${zone().name} (${S.x},${S.y}) 朝${DN[S.d]} 金幣 ${S.gold} 藥���${S.items.potion} 魔水${S.items.ether}`;
|
| 302 |
+
}
|
| 303 |
+
function renderAll(){drawScene();renderParty();drawAuto()}
|
| 304 |
+
|
| 305 |
+
/* ---------- 移動 ---------- */
|
| 306 |
+
function turn(t){if(!canAct())return;S.d=(S.d+t+4)%4;markVis();renderAll()}
|
| 307 |
+
const canAct=()=>S&&S.mode==='explore'&&!MENU;
|
| 308 |
+
function move(rel){
|
| 309 |
+
if(!canAct())return;
|
| 310 |
+
const d=(S.d+rel)%4,nx=S.x+DX[d],ny=S.y+DY[d],c=cellAt(S.map,nx,ny);
|
| 311 |
+
if(isSolid(c)){bump(c,nx,ny,d);renderAll();return}
|
| 312 |
+
S.x=nx;S.y=ny;S.steps++;if(S.cd>0)S.cd--;markVis();
|
| 313 |
+
renderAll();
|
| 314 |
+
if(event(c,nx,ny))return;
|
| 315 |
+
const z=zone(),ch=z.tier===0?1/14:1/10;
|
| 316 |
+
if(S.cd<=0&&Math.random()<ch)randomFight();
|
| 317 |
+
}
|
| 318 |
+
function bump(c,x,y,d){
|
| 319 |
+
if(c==='#'||c==='w'){say(c==='w'?'水太深了,過不去。':'前面被擋住了。');return}
|
| 320 |
+
if(c==='D'){setCell(x,y,'.');say('你推開了門。');return}
|
| 321 |
+
if(c==='T'){S.mode='town';renderAll();townMenu();return}
|
| 322 |
+
if(c==='X'){showMenu('地牢出口','要離開這裡回到地表嗎?',[{t:'離開',f:()=>{const r=S.ret;S.map=r.map;S.x=r.x;S.y=r.y;S.d=r.d;S.cd=3;markVis();save();renderAll()}},{t:'留下'}]);return}
|
| 323 |
+
const dn={'1':'cellar','2':'crypt','3':'tower'}[c];
|
| 324 |
+
if(c==='2'&&!S.keys.silver){showMenu('地穴大門','大門被一把銀製的鎖封住了。\n(需要銀鑰匙)',OK());return}
|
| 325 |
+
if(c==='3'&&!S.keys.ember){showMenu('黑塔大門','大門上有一個燃燒的凹槽。\n(需要餘燼印記)',OK());return}
|
| 326 |
+
showMenu(ZONES[dn].name,`要進入「${ZONES[dn].name}」嗎?\n進入前最好先補給並存檔。`,[{t:'進入',f:()=>{S.ret={map:S.map,x:S.x,y:S.y,d:(d+2)%4};
|
| 327 |
+
const M=MAPS[dn];S.map=dn;S.x=1;S.y=1;S.d=1;S.cd=3;markVis();save();renderAll();say(`你進入了${ZONES[dn].name}。`,'g')}},{t:'還是算了'}]);
|
| 328 |
+
}
|
| 329 |
+
function event(c,x,y){
|
| 330 |
+
const z=zone();
|
| 331 |
+
if(c==='c'){setCell(x,y,'.');chest(z);renderAll();return true}
|
| 332 |
+
if(c==='H'){setCell(x,y,'.');P().forEach(p=>{if(p.hp>0){p.hp=p.mhp;p.sp=p.msp}});say('清澈的泉水讓全隊恢復了精力!','s');renderAll();return true}
|
| 333 |
+
if(c==='n'){setCell(x,y,'.');const ns=NOTES[S.map];say(ns?ns[rnd(ns.length)]:'一塊無字的石板。','g');return true}
|
| 334 |
+
if(c==='M'){setCell(x,y,'.');say('伏兵出現了!','b');startCombat(z.elite.concat(Math.random()<.5?[z.mons[rnd(4)]]:[]),{});return true}
|
| 335 |
+
if(c==='B'){say('你們面對守護者!','b');startCombat([z.boss],{boss:S.map});return true}
|
| 336 |
+
return false;
|
| 337 |
+
}
|
| 338 |
+
function chest(z){
|
| 339 |
+
const t=Math.max(1,z.tier),gold=rr(15,40)*t;S.gold+=gold;say(`打開寶箱,得到 ${gold} 金幣!`,'g');
|
| 340 |
+
if(Math.random()<.4){S.items.potion++;say('還找到了一瓶藥水。','g')}
|
| 341 |
+
if(Math.random()<.2){S.items.ether++;say('還找到了一瓶魔力水。','g')}
|
| 342 |
+
if(Math.random()<.2){const p=alive()[rnd(alive().length)];if(p){const d=rr(3,8)*t;p.hp=Math.max(1,p.hp-d);say(`陷阱!${p.name}受到 ${d} 點傷害。`,'b')}}
|
| 343 |
+
}
|
| 344 |
+
function randomFight(){
|
| 345 |
+
const z=zone(),n=Math.min(5,rr(1,z.tier===0?3:2+z.tier)),ids=[];
|
| 346 |
+
for(let i=0;i<n;i++)ids.push(z.mons[rnd(z.mons.length)]);
|
| 347 |
+
say('遭遇敵人!','b');startCombat(ids,{});
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
/* ---------- 戰鬥 ---------- */
|
| 351 |
+
function startCombat(ids,opts){
|
| 352 |
+
C={en:ids.map(id=>({...MON[id],id,mhp:MON[id].hp})),i:-1,opts,round:1};
|
| 353 |
+
S.mode='combat';P().forEach(p=>p.dg=false);
|
| 354 |
+
say('敵人:'+C.en.map(e=>e.n).join('、'));nextActor();
|
| 355 |
+
}
|
| 356 |
+
function nextActor(){
|
| 357 |
+
if(checkEnd())return;
|
| 358 |
+
C.i++;while(C.i<P().length&&P()[C.i].hp<=0)C.i++;
|
| 359 |
+
if(C.i>=P().length){enemyPhase();return}
|
| 360 |
+
cmdMain();renderAll();
|
| 361 |
+
}
|
| 362 |
+
function cmdMain(){
|
| 363 |
+
const p=P()[C.i];
|
| 364 |
+
setCmd([[`${p.name}:攻擊`,()=>pickEnemy(e=>{attack(p,e);nextActor()})],['施法',()=>cmdSpell(p)],['物品',()=>cmdItem(p)],['防禦',()=>{p.dg=true;say(`${p.name}採取防禦姿態。`);nextActor()}],['逃跑',()=>run(p)]]);
|
| 365 |
+
}
|
| 366 |
+
function pickEnemy(cb){
|
| 367 |
+
const l=C.en.filter(e=>e.hp>0);if(l.length===1){cb(l[0]);return}
|
| 368 |
+
setCmd(l.map(e=>[`${e.n} (${e.hp})`,()=>cb(e)]).concat([['返回',cmdMain]]));
|
| 369 |
+
}
|
| 370 |
+
function pickAlly(cb,dead){
|
| 371 |
+
setCmd(P().filter(p=>dead?p.hp<=0:p.hp>0).map(p=>[`${p.name} (${p.hp}/${p.mhp})`,()=>cb(p)]).concat([['返回',cmdMain]]));
|
| 372 |
+
}
|
| 373 |
+
const physDmg=p=>atkOf(p)+p.w+rr(1,6);
|
| 374 |
+
function dmgEnemy(e,d,pierce){
|
| 375 |
+
d=Math.max(1,d-(pierce?0:e.d));e.hp-=d;return d;
|
| 376 |
+
}
|
| 377 |
+
function attack(p,e){
|
| 378 |
+
if(Math.random()<.1){say(`${p.name}揮空了。`);return}
|
| 379 |
+
const d=dmgEnemy(e,physDmg(p));say(`${p.name}攻擊${e.n},造成 ${d} 點傷害。`);if(e.hp<=0)say(`${e.n}倒下了!`,'g');
|
| 380 |
+
}
|
| 381 |
+
function cmdSpell(p){
|
| 382 |
+
const sp=CLS[p.cls].spells.filter(s=>s.lv<=p.lv);
|
| 383 |
+
setCmd(sp.map(s=>[`${s.n}(${s.c})`,()=>cast(p,s),p.sp<s.c]).concat([['返回',cmdMain]]));
|
| 384 |
+
}
|
| 385 |
+
function cast(p,s){
|
| 386 |
+
const fin=()=>{p.sp-=s.c;nextActor()},lv=p.lv,liv=()=>C.en.filter(e=>e.hp>0);
|
| 387 |
+
const hit=(e,d,pierce)=>{const x=dmgEnemy(e,d,pierce);say(`${p.name}的${s.n}命中${e.n},${x} 點傷害。`);if(e.hp<=0)say(`${e.n}倒下了!`,'g')};
|
| 388 |
+
switch(s.k){
|
| 389 |
+
case 'power':pickEnemy(e=>{hit(e,Math.floor(physDmg(p)*1.8));fin()});break;
|
| 390 |
+
case 'spark':pickEnemy(e=>{hit(e,6+lv*3+rnd(6),true);fin()});break;
|
| 391 |
+
case 'holy':pickEnemy(e=>{hit(e,Math.floor((5+lv*2.5+rnd(6))*(e.und?1.8:1)),true);fin()});break;
|
| 392 |
+
case 'fire':liv().forEach(e=>hit(e,Math.floor(4+lv*2.5+rnd(6)),true));fin();break;
|
| 393 |
+
case 'volley':for(let i=0;i<2;i++){const l=liv();if(l.length)hit(l[rnd(l.length)],Math.floor(physDmg(p)*.9))}fin();break;
|
| 394 |
+
case 'heal':pickAlly(t=>{const h=14+lv*4;t.hp=Math.min(t.mhp,t.hp+h);say(`${p.name}治療了${t.name},恢復 ${h} HP。`,'s');fin()});break;
|
| 395 |
+
case 'mheal':alive().forEach(t=>{const h=10+lv*3;t.hp=Math.min(t.mhp,t.hp+h)});say(`${p.name}施放群體治療!`,'s');fin();break;
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
function cmdItem(p){
|
| 399 |
+
setCmd([[`藥水 x${S.items.potion}`,()=>pickAlly(t=>{S.items.potion--;t.hp=Math.min(t.mhp,t.hp+40);say(`${t.name}喝下藥水,恢復 40 HP。`,'s');nextActor()}),S.items.potion<1],
|
| 400 |
+
[`魔力水 x${S.items.ether}`,()=>pickAlly(t=>{S.items.ether--;t.sp=Math.min(t.msp,t.sp+15);say(`${t.name}喝下魔力水。`,'s');nextActor()}),S.items.ether<1],['返回',cmdMain]]);
|
| 401 |
+
}
|
| 402 |
+
function run(p){
|
| 403 |
+
if(C.opts.boss){say('無法從守護者面前逃走!','b');nextActor();return}
|
| 404 |
+
if(Math.random()<.55){say('全隊成功逃脫。');endCombat();}else{say(`${p.name}沒能逃掉。`);nextActor()}
|
| 405 |
+
}
|
| 406 |
+
function hurt(p,d,pierce){
|
| 407 |
+
d=Math.max(1,d-(pierce?0:p.a+(p.dg?3+Math.floor(p.lv/2):0)));p.hp-=d;say(`${p.name}受到 ${d} 點傷害。`,'b');
|
| 408 |
+
if(p.hp<=0){p.hp=0;say(`${p.name}倒下了!`,'b')}
|
| 409 |
+
}
|
| 410 |
+
function enemyPhase(){
|
| 411 |
+
for(const e of C.en){
|
| 412 |
+
if(e.hp<=0)continue;const al=alive();if(!al.length)break;
|
| 413 |
+
if(e.aoe&&Math.random()<.3){say(`${e.n}施放黑暗風暴!`,'b');al.forEach(p=>hurt(p,rr(e.a[0],e.a[1])-4,true));continue}
|
| 414 |
+
const t=al[rnd(al.length)];
|
| 415 |
+
if(Math.random()<.85)hurt(t,rr(e.a[0],e.a[1]));else say(`${e.n}攻擊${t.name}但沒有命中。`);
|
| 416 |
+
}
|
| 417 |
+
P().forEach(p=>p.dg=false);C.i=-1;C.round++;nextActor();
|
| 418 |
+
}
|
| 419 |
+
function checkEnd(){
|
| 420 |
+
if(!alive().length){gameOver();return true}
|
| 421 |
+
if(C.en.every(e=>e.hp<=0)){victory();return true}
|
| 422 |
+
return false;
|
| 423 |
+
}
|
| 424 |
+
function endCombat(){S.mode='explore';S.cd=3;C=null;cmdExplore();renderAll()}
|
| 425 |
+
function victory(){
|
| 426 |
+
const xp=C.en.reduce((a,e)=>a+e.xp,0),g=C.en.reduce((a,e)=>a+e.g,0),boss=C.opts.boss;
|
| 427 |
+
S.gold+=g;say(`勝利!每人獲得 ${xp} 經驗,戰利品 ${g} 金幣。`,'g');
|
| 428 |
+
alive().forEach(p=>{p.xp+=xp;while(p.xp>=35*p.lv*p.lv){p.lv++;const c=CLS[p.cls],o=p.msp;p.mhp+=Math.round(c.hp[1])+rnd(3);p.msp=mspOf(p);p.hp=Math.min(p.mhp,p.hp+Math.round(c.hp[1]));p.sp=Math.min(p.msp,p.sp+p.msp-o);say(`${p.name}升到了 ${p.lv} 級!`,'g')}});
|
| 429 |
+
const b=C.opts.boss;endCombat();
|
| 430 |
+
if(b==='cellar'){setCell(MAPS.cellar.far[0],MAPS.cellar.far[1],'.');S.keys.silver=true;showMenu('地精王倒下了','你們從地精王的肚子裡找到了【銀鑰匙】!\n它能打開原野東北方的灰燼地穴。',OK(save))}
|
| 431 |
+
else if(b==='crypt'){setCell(MAPS.crypt.far[0],MAPS.crypt.far[1],'.');S.keys.ember=true;showMenu('灰燼騎士安息了','騎士的鎧甲碎裂,露出一枚燃燒的【餘燼印記】。\n它能開啟原野中央的弗拉格之塔。',OK(save))}
|
| 432 |
+
else if(b==='tower'){S.won=true;save();showMenu('太陽再度升起','弗拉格倒下,太陽水晶的碎片在塔頂重聚,迸出萬丈金光。\n瓦恩的天空恢復了藍色,索皮加爾鎮傳來歡呼聲。\n\n~ 全劇終 ~\n(你仍可繼續探索與練功)',[{t:'繼續冒險'},{t:'重新開始',f:()=>titleMenu(true)}])}
|
| 433 |
+
}
|
| 434 |
+
function gameOver(){
|
| 435 |
+
S.mode='explore';C=null;renderAll();setCmd([]);
|
| 436 |
+
showMenu('全軍覆沒','你們的冒險在此中斷……',[{t:'讀取存檔',f:()=>{const s=load();if(s){S=s;S.mode='explore';cmdExplore();renderAll()}else titleMenu(true)}},{t:'重新開始',f:()=>titleMenu(true)}]);
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
/* ---------- 城鎮 ---------- */
|
| 440 |
+
function leaveTown(){S.mode='explore';S.d=2;S.cd=3;markVis();save();cmdExplore();renderAll()}
|
| 441 |
+
function townMenu(){
|
| 442 |
+
setCmd([]);
|
| 443 |
+
showMenu('索皮加爾鎮',`金幣:${S.gold}\n要去哪裡?`,[
|
| 444 |
+
{t:'旅店 — 休息並存檔 (10G)',f:inn},{t:'神殿 — 復活 / 治療',f:temple},{t:'商店 — 武器 / 防具 / 藥水',f:shop},{t:'酒館 — 打聽消息',f:tavern},{t:'離開城鎮',f:leaveTown}]);
|
| 445 |
+
}
|
| 446 |
+
function inn(){
|
| 447 |
+
if(S.gold<10){showMenu('旅店','老闆搖頭:「沒錢不能住。」',OK(townMenu));return}
|
| 448 |
+
S.gold-=10;P().forEach(p=>{if(p.hp>0){p.hp=p.mhp;p.sp=p.msp}});save();renderAll();
|
| 449 |
+
showMenu('旅店','你們睡了一覺,所有存活的隊員恢復了體力。\n(已存檔)',OK(townMenu));
|
| 450 |
+
}
|
| 451 |
+
function temple(){
|
| 452 |
+
const dead=P().filter(p=>p.hp<=0);
|
| 453 |
+
const opts=dead.map(p=>({t:`復活 ${p.name} (${60*p.lv}G)`,f:()=>{const c=60*p.lv;if(S.gold<c){showMenu('神殿','金幣不夠。',OK(temple));return}S.gold-=c;p.hp=Math.ceil(p.mhp/2);renderAll();showMenu('神殿',`${p.name}重回人間!`,OK(temple))}}));
|
| 454 |
+
opts.push({t:'祈福 — 治療全隊 HP (5G)',f:()=>{if(S.gold<5){showMenu('神殿','金幣不夠。',OK(temple));return}S.gold-=5;P().forEach(p=>{if(p.hp>0)p.hp=p.mhp});renderAll();showMenu('神殿','溫暖的光芒治癒了你們。',OK(temple))}});
|
| 455 |
+
opts.push({t:'返回',f:townMenu});showMenu('神殿',`金幣:${S.gold}`,opts);
|
| 456 |
+
}
|
| 457 |
+
function shop(){
|
| 458 |
+
showMenu('商店',`金幣:${S.gold}`,[
|
| 459 |
+
{t:'藥水 (20G) 恢復40HP',f:()=>buyItem('potion',20)},{t:'魔力水 (30G) 恢復15SP',f:()=>buyItem('ether',30)},
|
| 460 |
+
{t:'武器',f:()=>gear('w',WEAP,'wn')},{t:'防具',f:()=>gear('a',ARM,'an')},{t:'返回',f:townMenu}]);
|
| 461 |
+
}
|
| 462 |
+
function buyItem(k,p){if(S.gold<p){showMenu('商店','金幣不夠。',OK(shop));return}S.gold-=p;S.items[k]++;renderAll();shop()}
|
| 463 |
+
function gear(f,list,nf){
|
| 464 |
+
showMenu(f==='w'?'武器店':'防具店',`金幣:${S.gold}`,list.map(it=>({t:`${it.n} +${it.b} (${it.p}G)`,f:()=>
|
| 465 |
+
showMenu('裝備給誰?',it.n,P().map(p=>({t:`${p.name} (${p[f]>=it.b?'已有更好':'+'+p[f]})`,f:()=>{
|
| 466 |
+
if(p[f]>=it.b){showMenu('商店','他已經有更好的裝備了。',OK(()=>gear(f,list,nf)));return}
|
| 467 |
+
if(S.gold<it.p){showMenu('商店','金幣不夠。',OK(()=>gear(f,list,nf)));return}
|
| 468 |
+
S.gold-=it.p;p[f]=it.b;p[nf]=it.n;renderAll();gear(f,list,nf)}})).concat([{t:'取消',f:()=>gear(f,list,nf)}]))})).concat([{t:'返回',f:shop}]));
|
| 469 |
+
}
|
| 470 |
+
function tavern(){
|
| 471 |
+
let t='酒保壓低聲音:\n';
|
| 472 |
+
if(S.won)t+='「英雄!太陽又升起來了!今晚的酒算我的。」';
|
| 473 |
+
else if(!S.keys.silver)t+='「天空自從太陽水晶碎裂後就一直灰濛濛的。大法師弗拉格偷走了碎片……\n聽說西北方的鼠穴地窖裡,地精王吞了一把銀鑰匙。」';
|
| 474 |
+
else if(!S.keys.ember)t+='「有了銀鑰匙,就能進入東北方的灰燼地穴。那裡的騎士守著一枚餘燼印記。」';
|
| 475 |
+
else t+='「餘燼印記能開啟原野中央的黑塔。弗拉格就在頂端……祝你們好運。」';
|
| 476 |
+
showMenu('酒館',t+'\n\n(提示:先在原野練功,買裝備再下地牢。聖光對不死者特別有效。)',OK(townMenu));
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
/* ---------- 物品 ---------- */
|
| 480 |
+
function itemMenu(){
|
| 481 |
+
if(!canAct())return;
|
| 482 |
+
showMenu('物品',`藥水 x${S.items.potion} 魔力水 x${S.items.ether}`,[
|
| 483 |
+
{t:'使用藥水 (+40HP)',f:()=>useOn('potion')},{t:'使用魔力水 (+15SP)',f:()=>useOn('ether')},{t:'關閉'}]);
|
| 484 |
+
}
|
| 485 |
+
function useOn(k){
|
| 486 |
+
if(S.items[k]<1){showMenu('物品','沒有了。',OK(itemMenu));return}
|
| 487 |
+
showMenu('對誰使用?','',P().filter(p=>p.hp>0).map(p=>({t:`${p.name} HP${p.hp}/${p.mhp} SP${p.sp}/${p.msp}`,f:()=>{
|
| 488 |
+
S.items[k]--;if(k==='potion')p.hp=Math.min(p.mhp,p.hp+40);else p.sp=Math.min(p.msp,p.sp+15);renderAll();itemMenu()}})).concat([{t:'返回',f:itemMenu}]));
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
/* ---------- 標題 / 故事 ---------- */
|
| 492 |
+
function startGame(s){
|
| 493 |
+
S=s;S.mode='explore';C=null;markVis();cmdExplore();renderAll();
|
| 494 |
+
}
|
| 495 |
+
function titleMenu(fresh){
|
| 496 |
+
S=null;C=null;setCmd([]);renderAll();$('log').innerHTML='';
|
| 497 |
+
const opts=[{t:'新遊戲',f:()=>{startGame(newState());showMenu('序章',STORY,OK(()=>{say('你們站在索皮加爾鎮外的原野上。北方就是鎮子。','g');save()}))}}];
|
| 498 |
+
if(load())opts.push({t:'繼續遊戲',f:()=>{startGame(load());say('讀取存檔。','g')}});
|
| 499 |
+
showMenu('魔法門傳說:太陽水晶','一款向《魔法門 I》致敬的第一人稱迷宮 RPG。',opts);
|
| 500 |
+
}
|
| 501 |
+
const STORY='瓦恩大陸的天空曾有一顆永不熄滅的太陽水晶。\n\n大法師科拉克的叛徒學徒弗拉格,在一夜之間擊碎了水晶,將碎片帶回自己的黑塔。從那天起,天空染成灰色,怪物從地底湧出。\n\n你們四位冒險者受索皮加爾鎮的長老之託,要找回碎片。\n\n目標:鼠穴地窖的地精王 → 灰燼地穴的灰燼騎士 → 弗拉格之塔。\n\n先到北方的鎮子補給吧!';
|
| 502 |
+
|
| 503 |
+
/* ---------- 鍵盤 ---------- */
|
| 504 |
+
document.addEventListener('keydown',e=>{
|
| 505 |
+
const k=e.key;
|
| 506 |
+
if(MENU){if(/^[1-9]$/.test(k))pick(+k-1);return}
|
| 507 |
+
if(!S||S.mode!=='explore')return;
|
| 508 |
+
const m={ArrowUp:()=>move(0),w:()=>move(0),W:()=>move(0),ArrowDown:()=>move(2),s:()=>move(2),S:()=>move(2),ArrowLeft:()=>turn(-1),a:()=>turn(-1),A:()=>turn(-1),ArrowRight:()=>turn(1),d:()=>turn(1),D:()=>turn(1),q:()=>move(3),Q:()=>move(3),e:()=>move(1),E:()=>move(1),i:itemMenu,I:itemMenu,m:()=>{am.style.display=am.style.display==='none'?'block':'none'},M:()=>{am.style.display=am.style.display==='none'?'block':'none'}};
|
| 509 |
+
if(m[k]){e.preventDefault();m[k]()}
|
| 510 |
+
});
|
| 511 |
+
|
| 512 |
+
window.__G={get S(){return S},get C(){return C},MAPS,MON,startGame,newState,move,turn,pick,get MENU(){return MENU},checkMaps(){
|
| 513 |
+
const out={};for(const k in DUN){const M=MAPS[k];const seen=new Set(['1,1']),q=[[1,1]];for(let i=0;i<q.length;i++){const[x,y]=q[i];for(let d=0;d<4;d++){const nx=x+DX[d],ny=y+DY[d];const c=M.g[ny][nx];if(c!=='#'&&c!=='X'&&!seen.has(nx+','+ny)){seen.add(nx+','+ny);q.push([nx,ny])}}}
|
| 514 |
+
out[k]={boss:seen.has(M.far.join(',')),cells:seen.size}}return out}};
|
| 515 |
+
titleMenu();
|
| 516 |
+
})();
|
index.html
CHANGED
|
@@ -1,19 +1,32 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 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">
|
| 6 |
+
<title>WMM1 · 魔法門傳說:太陽水晶</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="app">
|
| 11 |
+
<div id="left">
|
| 12 |
+
<canvas id="view" width="320" height="200"></canvas>
|
| 13 |
+
<div id="cmd"></div>
|
| 14 |
+
<div id="hint">WASD/方向鍵移動 Q/E 側移 I 物品 M 地圖 S 後退</div>
|
| 15 |
+
</div>
|
| 16 |
+
<div id="right">
|
| 17 |
+
<div id="where"></div>
|
| 18 |
+
<div id="party"></div>
|
| 19 |
+
<canvas id="auto" width="160" height="160"></canvas>
|
| 20 |
+
<div id="log"></div>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
<div id="overlay" hidden>
|
| 24 |
+
<div id="box">
|
| 25 |
+
<h2 id="ovTitle"></h2>
|
| 26 |
+
<div id="ovText"></div>
|
| 27 |
+
<div id="ovOpts"></div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
<script src="game.js"></script>
|
| 31 |
+
</body>
|
| 32 |
</html>
|
style.css
CHANGED
|
@@ -1,28 +1,32 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*{box-sizing:border-box}
|
| 2 |
+
body{margin:0;background:#0b0710;color:#e8dcc0;font-family:"Courier New","Noto Sans Mono CJK TC",monospace;font-size:14px}
|
| 3 |
+
#app{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;padding:10px;max-width:1040px;margin:0 auto}
|
| 4 |
+
#left{flex:0 1 640px;min-width:300px}
|
| 5 |
+
#right{flex:1 1 300px;min-width:280px;display:flex;flex-direction:column;gap:6px}
|
| 6 |
+
#view{width:100%;aspect-ratio:8/5;image-rendering:pixelated;border:4px double #8a6a2a;background:#000;display:block}
|
| 7 |
+
#cmd{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px;min-height:40px}
|
| 8 |
+
#hint{color:#8a7a5a;font-size:12px;margin-top:4px}
|
| 9 |
+
button{background:#2a1c3a;color:#f0e0b0;border:2px outset #8a6a2a;padding:7px 10px;font:inherit;cursor:pointer}
|
| 10 |
+
button:hover{background:#432a5a}
|
| 11 |
+
button:active{border-style:inset}
|
| 12 |
+
button:disabled{opacity:.4}
|
| 13 |
+
#where{color:#f0c030;font-weight:bold}
|
| 14 |
+
#party{display:flex;flex-direction:column;gap:3px}
|
| 15 |
+
.pc{border:2px solid #4a3a2a;background:#160f20;padding:3px 6px;font-size:13px}
|
| 16 |
+
.pc.act{border-color:#f0c030;background:#2a2010}
|
| 17 |
+
.pc.dead{color:#e05050;opacity:.7}
|
| 18 |
+
.bar{height:5px;background:#2a2020;margin-top:2px}
|
| 19 |
+
.bar i{display:block;height:100%;background:#d04040}
|
| 20 |
+
.bar.sp i{background:#4080e0}
|
| 21 |
+
.eq{color:#8a7a5a;font-size:11px}
|
| 22 |
+
#auto{width:160px;height:160px;image-rendering:pixelated;border:2px solid #4a3a2a;background:#050308;align-self:flex-start}
|
| 23 |
+
#log{flex:1;min-height:120px;max-height:200px;overflow-y:auto;border:2px solid #4a3a2a;background:#0e0a14;padding:5px;font-size:13px;line-height:1.4}
|
| 24 |
+
#log .g{color:#f0c030}#log .b{color:#ff7070}#log .s{color:#70c0ff}
|
| 25 |
+
#overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);display:flex;align-items:center;justify-content:center;z-index:9}
|
| 26 |
+
#overlay[hidden]{display:none}
|
| 27 |
+
#box{background:#160f20;border:4px double #c8a040;padding:16px;max-width:520px;width:92%;max-height:90vh;overflow-y:auto}
|
| 28 |
+
#box h2{margin:0 0 8px;color:#f0c030}
|
| 29 |
+
#ovText{white-space:pre-wrap;line-height:1.5;margin-bottom:10px}
|
| 30 |
+
#ovOpts{display:flex;flex-direction:column;gap:6px}
|
| 31 |
+
#ovOpts button{text-align:left}
|
| 32 |
+
@media(max-width:700px){#auto{display:none}}
|