🎮ArcadeLab

音乐之战|网页联机版

by FlashLion33
308 lines10.9 KB
▶ Play
<!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.