音乐之战|网页联机版
by FlashLion33308 lines10.9 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音乐之战|网页联机版</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:system-ui}
body{background:#0b0b1a;color:#fff;min-height:100vh}
#app{max-width:720px;margin:0 auto;padding:12px}
.btn{background:#6328e8;border:none;color:#fff;padding:12px 16px;border-radius:10px;font-size:16px;margin:6px;cursor:pointer}
.btn:active{opacity:0.8}
.btn-red{background:#d82848}
.btn-green{background:#20b864}
.card{background:#171730;border-radius:12px;padding:16px;margin:10px 0}
.hidden{display:none !important}
.flex{display:flex;gap:8px;flex-wrap:wrap}
.battle-ui{position:relative}
.hp-bar{height:14px;background:#222;border-radius:7px;overflow:hidden;margin:4px 0}
.hp-fill{height:100%;background:#28d060;transition:0.3s}
.energy-bar{height:10px;background:#222;border-radius:5px;overflow:hidden}
.energy-fill{height:100%;background:#42a5f5;transition:0.3s}
.demon-bar{height:10px;background:#222;border-radius:5px;overflow:hidden}
.demon-fill{height:100%;background:#9c27b0;transition:0.3s}
.room-code{font-size:24px;letter-spacing:4px;padding:10px;background:#222;border-radius:8px;text-align:center;margin:10px 0}
input{padding:10px;border-radius:8px;border:none;font-size:16px;width:100%;max-width:300px;margin:6px 0}
.char-item{padding:10px;background:#242442;border-radius:10px;margin:6px 0;cursor:pointer}
.char-item.selected{outline:2px solid #9c27b0}
.avatar-box{width:120px;height:120px;background:#222;border-radius:10px;overflow:hidden;margin:6px auto}
.avatar-box img{width:100%;height:100%;object-fit:cover}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<script>
const Game={
state:"menu",
myChar:null,enemyChar:null,
battle:null,net:{peer:null,conn:null,roomId:""},
save:{win:0,lose:0,chars:{}},
init(){
const s=localStorage.getItem("musicWarSave");
if(s) this.save=JSON.parse(s);
this.render();
},
saveData(){localStorage.setItem("musicWarSave",JSON.stringify(this.save))},
render(){
const app=document.getElementById("app");
if(this.state==="menu"){
app.innerHTML=`
<div class="card">
<h1>🎵 音乐之战</h1>
<p>胜:${this.save.win}|负:${this.save.lose}</p>
<button class="btn" onclick="Game.goSelect()">单机对战</button>
<button class="btn" onclick="Game.goNetLobby()">联机对战</button>
<button class="btn" onclick="Game.goBook()">角色图鉴(上传贴图)</button>
<button class="btn" onclick="Game.goHelp()">玩法说明</button>
</div>`;
}else if(this.state==="netLobby"){
app.innerHTML=`
<div class="card">
<h2>联机房间</h2>
<div>
<button class="btn-green btn" onclick="Game.createRoom()">创建房间</button>
</div>
<div>
<p>输入房间码加入好友</p>
<input id="roomInput" placeholder="6位房间码">
<button class="btn" onclick="Game.joinRoom()">加入房间</button>
</div>
<div id="roomShow"></div>
<button class="btn-red btn" onclick="Game.backMenu()">返回主菜单</button>
</div>`;
}else if(this.state==="select"){
const chars=[
{id:"chihu",name:"赤狐",hp:100,atk:28,desc:"爆发输出"},
{id:"echo",name:"回音",hp:90,atk:22,desc:"连击流"},
{id:"dixin",name:"狄鑫",hp:110,atk:18,desc:"控制坦度"}
];
let html=`<div class="card"><h2>选择角色</h2>`;
chars.forEach(c=>{
html+=`<div class="char-item ${this.myChar?.id===c.id?'selected':''}" onclick="Game.pickChar('${c.id}')">
<h3>${c.name}</h3><p>${c.desc} HP:${c.hp}</p>
</div>`;
});
html+=`<button class="btn-green btn" onclick="Game.startBattle()">开始战斗</button>
<button class="btn-red btn" onclick="Game.backMenu()">返回</button></div>`;
app.innerHTML=html;
}else if(this.state==="battle"){
this.renderBattle(app);
}else if(this.state==="book"){
app.innerHTML=`
<div class="card">
<h2>角色图鉴 上传贴图</h2>
<p>选择图片替换角色立绘,保存在本机浏览器</p>
<div>
<p>赤狐贴图</p>
<input type="file" accept="image/*" onchange="Game.uploadImg('chihu',this)">
</div>
<div>
<p>回音贴图</p>
<input type="file" accept="image/*" onchange="Game.uploadImg('echo',this)">
</div>
<div>
<p>狄鑫贴图</p>
<input type="file" accept="image/*" onchange="Game.uploadImg('dixin',this)">
</div>
<button class="btn-red btn" onclick="Game.backMenu()">返回</button>
</div>`;
}else if(this.state==="help"){
app.innerHTML=`
<div class="card">
<h2>玩法说明</h2>
<p>回合制音乐对战,积攒恶魔值,满100触发恶魔抽取爆发</p>
<p>联机提示:公共PeerJS服务器偶尔连不上,切换流量/Wi‑Fi重试</p>
<button class="btn-red btn" onclick="Game.backMenu()">返回</button>
</div>`;
}else if(this.state==="result"){
const win=this.battle.winner==="self";
app.innerHTML=`
<div class="card">
<h2>${win?"🎉胜利":"💔失败"}</h2>
<p>我方血量:${this.battle.self.hp}</p>
<p>敌方血量:${this.battle.enemy.hp}</p>
<button class="btn-green btn" onclick="Game.goSelect()">再来一局</button>
<button class="btn-red btn" onclick="Game.backMenu()">返回主菜单</button>
</div>`;
}
},
uploadImg(charId,input){
const file=input.files[0];
if(!file)return;
const reader=new FileReader();
reader.onload=(e)=>{
this.save.chars[charId]=e.target.result;
this.saveData();
alert("贴图保存成功!战斗界面生效");
};
reader.readAsDataURL(file);
},
pickChar(id){
const map={
"chihu":{id:"chihu",name:"赤狐",hp:100,atk:28},
"echo":{id:"echo",name:"回音",hp:90,atk:22},
"dixin":{id:"dixin",name:"狄鑫",hp:110,atk:18}
};
this.myChar=map[id];
this.render();
},
goSelect(){this.state="select";this.render()},
goNetLobby(){this.state="netLobby";this.render()},
goBook(){this.state="book";this.render()},
goHelp(){this.state="help";this.render()},
backMenu(){
this.state="menu";
if(this.net.peer){this.net.peer.destroy();this.net.peer=null}
this.render();
},
startBattle(){
if(!this.myChar){alert("请先选择角色");return}
this.battle={
self:{...this.myChar,energy:20,demon:0},
enemy:{id:"ai",name:"AI对手",hp:100,atk:22,energy:20,demon:0},
turn:1,isPlayerTurn:true,winner:null,isNet:false
};
this.state="battle";
this.render();
this.aiTurn();
},
renderBattle(app){
const b=this.battle;
const self=b.self,en=b.enemy;
const selfImg=this.save.chars[self.id]||"";
const enImg=this.save.chars[en.id]||"";
app.innerHTML=`
<div class="card battle-ui">
<h3>敌方:${en.name}</h3>
<div class="avatar-box">${enImg?`<img src="${enImg}">`:""}</div>
<div class="hp-bar"><div class="hp-fill" style="width:${en.hp}%"></div></div>
<p>HP ${en.hp}</p>
<div class="energy-bar"><div class="energy-fill" style="width:${en.energy}%"></div></div>
<div class="demon-bar"><div class="demon-fill" style="width:${en.demon}%"></div></div>
<hr style="margin:12px 0">
<h3>我方:${self.name}</h3>
<div class="avatar-box">${selfImg?`<img src="${selfImg}">`:""}</div>
<div class="hp-bar"><div class="hp-fill" style="width:${self.hp}%"></div></div>
<p>HP ${self.hp}</p>
<div class="energy-bar"><div class="energy-fill" style="width:${self.energy}%"></div></div>
<div class="demon-bar"><div class="demon-fill" style="width:${self.demon}%"></div></div>
<p>回合 ${b.turn}|${b.isPlayerTurn?"你的回合":"对手行动中"}</p>
<div class="flex">
<button class="btn" onclick="Game.doAction('attack')" ${!b.isPlayerTurn?"disabled":""}>普通攻击</button>
<button class="btn" onclick="Game.doAction('skill')" ${!b.isPlayerTurn||self.energy<30?"disabled":""}>技能攻击(30能量)</button>
<button class="btn" onclick="Game.doAction('defend')" ${!b.isPlayerTurn?"disabled":""}>防御</button>
<button class="btn-red btn" onclick="Game.forfeit()">认输</button>
</div>
</div>`;
},
doAction(type){
const b=this.battle;
if(!b.isPlayerTurn||b.winner)return;
const s=b.self,e=b.enemy;
if(type==="attack"){
const dmg=s.atk;
e.hp-=dmg;
e.demon+=12;
s.energy+=10;
}else if(type==="skill"){
if(s.energy<30)return;
s.energy-=30;
const dmg=s.atk*1.8;
e.hp-=dmg;
e.demon+=20;
}else if(type==="defend"){
s.demon+=8;
s.energy+=15;
}
this.checkDemon(s);
this.checkDemon(e);
this.checkWin();
if(b.winner)return;
b.isPlayerTurn=false;
this.render();
setTimeout(()=>this.aiTurn(),800);
},
checkDemon(unit){
if(unit.demon>=100){
unit.demon=0;
unit.hp+=15;
unit.atk+=4;
}
},
checkWin(){
const b=this.battle;
if(b.self.hp<=0){b.winner="enemy"}
if(b.enemy.hp<=0){b.winner="self"}
if(b.winner){
if(b.winner==="self")this.save.win++;
else this.save.lose++;
this.saveData();
this.state="result";
this.render();
}
},
aiTurn(){
const b=this.battle;
if(b.winner)return;
const e=b.enemy,s=b.self;
const r=Math.random();
if(r<0.4){
s.hp-=e.atk;
s.demon+=12;
}else if(r<0.7&&e.energy>=30){
e.energy-=30;
s.hp-=e.atk*1.5;
s.demon+=18;
}else{
e.energy+=12;
e.demon+=8;
}
this.checkDemon(s);this.checkDemon(e);
this.checkWin();
if(b.winner)return;
b.turn++;
b.isPlayerTurn=true;
this.render();
},
forfeit(){
this.battle.winner="enemy";
this.save.lose++;
this.saveData();
this.state="result";
this.render();
},
createRoom(){
if(this.net.peer)return;
const peer=new Peer();
this.net.peer=peer;
peer.on("open",(id)=>{
this.net.roomId=id;
document.getElementById("roomShow").innerHTML=`<div class="room-code">房间码:${id}</div><p>等待好友加入...</p>`;
peer.on("connection",(conn)=>{
this.net.conn=conn;
alert("好友已加入!");
})
});
peer.on("error",err=>alert("联机出错:"+err.type))
},
joinRoom(){
const rid=document.getElementById("roomInput").value.trim();
if(!rid){alert("输入房间码");return}
const peer=new Peer();
this.net.peer=peer;
peer.on("open",()=>{
const conn=peer.connect(rid);
conn.on("open",()=>{
this.net.conn=conn;
alert("连接成功!");
});
conn.on("error",e=>alert("加入失败,检查房间码或网络"))
})
}
};
window.onload=()=>Game.init();
</script>
</body>
</html>Game Source: 音乐之战|网页联机版
Creator: FlashLion33
Libraries: none
Complexity: complex (308 lines, 10.9 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-flashlion33" to link back to the original. Then publish at arcadelab.ai/publish.