🎮ArcadeLab

怪物X联盟 · 单机版

by TurboBolt40
637 lines26.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>怪物X联盟 · 单机版</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  body {
    font-family:"Microsoft YaHei","PingFang SC",sans-serif;
    background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);
    color:#eee; min-height:100vh; padding:12px;
  }
  #app { max-width:880px; margin:0 auto; }
  h1 {
    text-align:center; font-size:26px; margin-bottom:4px;
    background:linear-gradient(90deg,#e94560,#f5a623);
    -webkit-background-clip:text; -webkit-text-fill-color:transparent;
    text-shadow:none; letter-spacing:2px;
  }
  .subtitle { text-align:center; color:#888; font-size:12px; margin-bottom:14px; }
  .panel {
    background:rgba(255,255,255,0.05); border-radius:12px; padding:14px;
    margin-bottom:12px; border:1px solid rgba(255,255,255,0.08);
    backdrop-filter:blur(4px);
  }
  .battlefield { display:flex; justify-content:space-between; align-items:flex-end; gap:12px; }
  .combatant { flex:1; }
  .combatant.foe { text-align:right; }
  .monster-art {
    font-size:64px; line-height:1; margin:6px 0;
    filter:drop-shadow(0 4px 6px rgba(0,0,0,.5));
  }
  .name-row { display:flex; justify-content:space-between; align-items:center; font-weight:bold; }
  .foe .name-row { flex-direction:row-reverse; }
  .lv { font-size:12px; color:#f5a623; }
  .hp-bar {
    height:14px; background:#333; border-radius:7px; overflow:hidden; margin:4px 0;
    border:1px solid rgba(255,255,255,.15);
  }
  .hp-fill {
    height:100%; background:linear-gradient(90deg,#2ecc71,#27ae60);
    transition:width .4s ease; border-radius:7px;
  }
  .hp-fill.mid { background:linear-gradient(90deg,#f5a623,#e67e22); }
  .hp-fill.low { background:linear-gradient(90deg,#e74c3c,#c0392b); }
  .hp-text { font-size:11px; color:#aaa; }
  .type-badge {
    display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px;
    margin-left:6px; color:#fff; font-weight:normal;
  }
  .vs { font-size:24px; color:#e94560; align-self:center; font-weight:bold; }

  .skills { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:6px; }
  button {
    font-family:inherit; cursor:pointer; border:none; border-radius:8px;
    padding:10px 12px; font-size:14px; color:#fff; transition:.15s;
    background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.12);
  }
  button:hover:not(:disabled) { transform:translateY(-1px); filter:brightness(1.2); }
  button:active:not(:disabled) { transform:translateY(0); }
  button:disabled { opacity:.4; cursor:not-allowed; }
  .skill-btn { text-align:left; }
  .skill-btn .pp { float:right; color:#aaa; font-size:11px; }
  .skill-btn .cat {
    font-size:10px; padding:1px 6px; border-radius:8px; margin-right:6px;
  }
  .cat.phys { background:#c0392b; }
  .cat.magic { background:#2980b9; }
  .cat.status { background:#27ae60; }

  .action-row { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; }
  .btn-catch { background:linear-gradient(90deg,#e94560,#c62a74); }
  .btn-bag { background:linear-gradient(90deg,#16a085,#138d75); }
  .btn-run { background:linear-gradient(90deg,#7f8c8d,#636e72); }
  .btn-next { background:linear-gradient(90deg,#2980b9,#1f618d); }

  .log {
    height:120px; overflow-y:auto; font-size:13px; line-height:1.5;
    background:rgba(0,0,0,0.3); border-radius:8px; padding:8px 10px;
    border:1px solid rgba(255,255,255,0.06);
  }
  .log p { margin:2px 0; }
  .log .dmg { color:#e74c3c; }
  .log .heal { color:#2ecc71; }
  .log .info { color:#3498db; }
  .log .sys { color:#f5a623; }

  .map-bar { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
  .map-node {
    padding:4px 10px; border-radius:12px; font-size:12px;
    background:rgba(255,255,255,0.07); border:1px solid transparent;
  }
  .map-node.active { border-color:#f5a623; color:#f5a623; }
  .map-node.cleared { color:#2ecc71; }
  .map-node.boss { color:#e94560; font-weight:bold; }

  .team-strip { display:flex; gap:8px; flex-wrap:wrap; }
  .team-member {
    padding:6px 10px; border-radius:8px; font-size:12px;
    background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.1);
    cursor:pointer; transition:.15s;
  }
  .team-member:hover { border-color:#f5a623; }
  .team-member.active { border-color:#2ecc71; background:rgba(46,204,113,0.15); }
  .team-member.fainted { opacity:.4; }

  .modal-mask {
    position:fixed; inset:0; background:rgba(0,0,0,0.7);
    display:none; align-items:center; justify-content:center; z-index:50;
    padding:20px;
  }
  .modal-mask.show { display:flex; }
  .modal {
    background:#1a1a2e; border-radius:16px; padding:24px;
    max-width:520px; width:100%; max-height:85vh; overflow-y:auto;
    border:1px solid rgba(255,255,255,0.12);
  }
  .modal h2 { color:#f5a623; margin-bottom:12px; text-align:center; }
  .dex-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:8px; }
  .dex-entry {
    background:rgba(255,255,255,0.05); border-radius:8px; padding:8px;
    font-size:12px; border:1px solid rgba(255,255,255,0.08);
  }
  .dex-entry.locked { opacity:.35; }
  .stat-line { display:flex; justify-content:space-between; font-size:11px; color:#bbb; }
  .close-btn { margin-top:16px; width:100%; background:linear-gradient(90deg,#e94560,#c62a74); }

  .hint { font-size:11px; color:#666; text-align:center; margin-top:8px; }
  .topbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; }
  .coins { color:#f5a623; font-weight:bold; }
  .toolbar { display:flex; gap:6px; }
  .toolbar button { padding:6px 10px; font-size:12px; }
  @media(max-width:560px){
    .skills,.action-row { grid-template-columns:1fr 1fr; }
    .monster-art { font-size:48px; }
    h1 { font-size:20px; }
  }
</style>
</head>
<body>
<div id="app">
  <h1>⚔️ 怪物X联盟 · 单机版</h1>
  <div class="subtitle">魁星岛 · 训练师冒险 — 收集 · 养成 · 对战</div>

  <div class="panel">
    <div class="topbar">
      <div>🗺️ <span id="stageName">孢子林</span> · 第 <span id="stageIdx">1</span>/<span id="stageTotal">10</span> 关</div>
      <div class="coins">💰 G币: <span id="coins">100</span> | 🧪 精灵球: <span id="balls">5</span></div>
    </div>
    <div class="map-bar" id="mapBar"></div>
  </div>

  <div class="panel">
    <div class="battlefield">
      <div class="combatant foe">
        <div class="monster-art" id="foeArt">👾</div>
        <div class="name-row">
          <span id="foeName">野生怪物</span>
          <span class="lv">Lv.<span id="foeLv">1</span></span>
        </div>
        <span class="type-badge" id="foeType" style="background:#666">普通</span>
        <div class="hp-bar"><div class="hp-fill" id="foeHpBar"></div></div>
        <div class="hp-text"><span id="foeHp">30</span>/<span id="foeMaxHp">30</span></div>
      </div>
      <div class="vs">VS</div>
      <div class="combatant">
        <div class="monster-art" id="myArt">🐣</div>
        <div class="name-row">
          <span id="myName">??</span>
          <span class="lv">Lv.<span id="myLv">1</span></span>
        </div>
        <span class="type-badge" id="myType" style="background:#666">普通</span>
        <div class="hp-bar"><div class="hp-fill" id="myHpBar"></div></div>
        <div class="hp-text"><span id="myHp">30</span>/<span id="myMaxHp">30</span> · EXP <span id="myExp">0</span>/<span id="myNextExp">20</span></div>
      </div>
    </div>

    <div class="skills" id="skillPanel"></div>

    <div class="action-row" style="margin-top:10px">
      <button class="btn-catch" onclick="doCatch()">🧪 捕捉 (<span id="ballCount">5</span>)</button>
      <button class="btn-bag" onclick="usePotion()">🧪 药水 (<span id="potions">2</span>)</button>
      <button class="btn-run" onclick="runAway()">🏃 逃跑</button>
      <button class="btn-next" onclick="nextStage()" id="nextBtn" style="display:none">➡️ 下一关</button>
    </div>
  </div>

  <div class="panel">
    <div style="font-size:12px;color:#888;margin-bottom:4px">⚔️ 战斗日志</div>
    <div class="log" id="log"></div>
  </div>

  <div class="panel">
    <div style="font-size:12px;color:#888;margin-bottom:6px">🎒 我的队伍 (点击切换出战)</div>
    <div class="team-strip" id="teamStrip"></div>
    <div class="toolbar" style="margin-top:10px">
      <button onclick="openDex()">📖 图鉴</button>
      <button onclick="openHelp()">❓ 玩法</button>
      <button onclick="resetGame()">🔄 重置存档</button>
    </div>
    <div class="hint">提示:削弱敌方血量后再捕捉成功率更高 · 属性相克 ×1.2 伤害 · 进度自动保存</div>
  </div>
</div>

<!-- 图鉴 / 帮助 弹窗 -->
<div class="modal-mask" id="modalMask">
  <div class="modal" id="modalContent"></div>
</div>

<script>
/* ============ 数据层:系别 / 相克 / 技能 / 宠物 ============ */
const TYPES = {
  火:{color:'#e74c3c', art:'🔥'}, 水:{color:'#3498db', art:'💧'},
  草:{color:'#2ecc71', art:'🌿'}, 电:{color:'#f5d020', art:'⚡'},
  地:{color:'#b9770e', art:'⛰️'}, 飞行:{color:'#a8e6cf', art:'🕊️'},
  格斗:{color:'#e67e22', art:'🥊'}, 超能:{color:'#9b59b6', art:'🔮'},
  鬼:{color:'#34495e', art:'👻'}, 龙:{color:'#e84393', art:'🐉'},
  普通:{color:'#95a5a6', art:'⭐'}
};
// 相克表:atk -> 被克制的防守系(伤害×1.2)
const WEAK = {
  火:['草','飞行'], 水:['火','地'], 草:['水','地'],
  电:['水','飞行'], 地:['电','火','格斗'], 飞行:['草','虫'],
  格斗:['普通','超能'], 超能:['格斗','鬼'], 鬼:['超能','鬼'],
  龙:['龙'], 普通:[]
};
function typeMultiplier(atkType, defType){
  if(WEAK[atkType] && WEAK[atkType].includes(defType)) return 1.2;
  // 反向:若防守系克制攻击系,则伤害减半
  if(WEAK[defType] && WEAK[defType].includes(atkType)) return 0.6;
  return 1.0;
}

// 技能池
const SKILLS = {
  撞击:{cat:'phys', power:18, pp:25, type:'普通', desc:'基础物理攻击'},
  火花:{cat:'magic', power:22, pp:20, type:'火', desc:'喷出小火苗'},
  喷射火焰:{cat:'magic', power:40, pp:10, type:'火', desc:'强力火焰攻击'},
  水枪:{cat:'magic', power:22, pp:20, type:'水', desc:'高压水流'},
  冲浪:{cat:'magic', power:40, pp:10, type:'水', desc:'巨浪拍击'},
  飞叶快刀:{cat:'phys', power:22, pp:20, type:'草', desc:'锋利叶片切割'},
  阳光烈焰:{cat:'magic', power:45, pp:8, type:'草', desc:'汇聚日光爆发'},
  电击:{cat:'magic', power:22, pp:20, type:'电', desc:'释放电流'},
  十万伏特:{cat:'magic', power:42, pp:10, type:'电', desc:'狂暴雷电'},
  震级:{cat:'phys', power:24, pp:18, type:'地', desc:'引发地震'},
  大地之力:{cat:'magic', power:42, pp:10, type:'地', desc:'地脉能量爆发'},
  啄击:{cat:'phys', power:20, pp:22, type:'飞行', desc:'用尖喙啄击'},
  空气斩:{cat:'phys', power:38, pp:12, type:'飞行', desc:'空气刃攻击'},
  爆裂拳:{cat:'phys', power:24, pp:18, type:'格斗', desc:'猛烈拳击'},
  气合拳:{cat:'phys', power:42, pp:10, type:'格斗', desc:'蓄力重拳'},
  念力:{cat:'magic', power:22, pp:20, type:'超能', desc:'精神冲击'},
  精神强念:{cat:'magic', power:42, pp:10, type:'超能', desc:'强烈精神波'},
  暗影拳:{cat:'phys', power:22, pp:20, type:'鬼', desc:'幽冥拳击'},
  暗影球:{cat:'magic', power:40, pp:10, type:'鬼', desc:'怨念能量球'},
  龙息:{cat:'magic', power:24, pp:18, type:'龙', desc:'龙之吐息'},
  龙星群:{cat:'magic', power:45, pp:8, type:'龙', desc:'降下流星雨'},
  空手切:{cat:'phys', power:20, pp:22, type:'普通', desc:'手刀劈砍'},
  居合斩:{cat:'phys', power:38, pp:12, type:'普通', desc:'快速斩击'},
  铁壁:{cat:'status', power:0, pp:15, type:'普通', desc:'自身物防+5', buff:{def:5}},
  冥想:{cat:'status', power:0, pp:15, type:'超能', desc:'自身魔攻+5', buff:{matk:5}},
  治愈:{cat:'status', power:0, pp:8, type:'普通', desc:'恢复30%HP', heal:0.3}
};

// 宠物模板(基于怪物X联盟资料抽象设计,D~S 等阶)
const SPECIES = {
  // D阶
  喇叭芽:{tier:'D', type:'草', base:{hp:30,atk:35,def:26,spd:25,matk:30,mdef:17},
    art:'🌱', skills:['连环踢','飞叶快刀'], evolveTo:null, evolveLv:0,
    habitat:'孢子林', dex:'叶片如鞭,初入魁星岛的训练师伙伴。'},
  小海螺:{tier:'D', type:'普通', base:{hp:32,atk:26,def:28,spd:22,matk:35,mdef:18},
    art:'🐚', skills:['喷水','硬邦邦'], evolveTo:null, evolveLv:0,
    habitat:'陨星湖', dex:'壳硬如铁,湖畔常见的小生物。'},
  小蝙蝠:{tier:'D', type:'普通', base:{hp:32,atk:27,def:21,spd:39,matk:32,mdef:27},
    art:'🦇', skills:['吹风','吼叫'], evolveTo:null, evolveLv:0,
    habitat:'晶石山', dex:'振翅于洞穴,速度敏捷。'},
  // C阶
  黑鱼:{tier:'C', type:'普通', base:{hp:36,atk:30,def:32,spd:25,matk:46,mdef:36},
    art:'🐟', skills:['喷水'], evolveTo:null, evolveLv:0,
    habitat:'陨星湖', dex:'深水中的灵鱼,魔力充沛。'},
  小花蜗:{tier:'C', type:'普通', base:{hp:40,atk:44,def:35,spd:32,matk:37,mdef:28},
    art:'🐌', skills:['飞叶快刀','弹性皮肤'], evolveTo:'食人花', evolveLv:16,
    habitat:'神秘林', dex:'背壳开花,可进化为食人花。'},
  仙人球:{tier:'C', type:'普通', base:{hp:35,atk:40,def:38,spd:32,matk:32,mdef:30},
    art:'🌵', skills:['空手切','反击'], evolveTo:'仙人掌', evolveLv:18,
    habitat:'巨兽矿野', dex:'沙漠刺球,格斗天赋初显。'},
  // B阶
  不死鸟:{tier:'B', type:'火', base:{hp:38,atk:37,def:37,spd:39,matk:55,mdef:51},
    art:'🐦🔥', skills:['灵光一闪','沙幕'], evolveTo:null, evolveLv:0,
    habitat:'赫里奥斯', dex:'浴火之鸟,火焰的使者。'},
  食人花:{tier:'B', type:'普通', base:{hp:49,atk:66,def:30,spd:34,matk:43,mdef:42},
    art:'🌺', skills:['吸取','飞叶快刀','究极吸取'], evolveTo:null, evolveLv:0,
    habitat:'远征哨岗', dex:'由小花蜗进化,吞噬光线的猎手。'},
  魅灵:{tier:'B', type:'鬼', base:{hp:41,atk:34,def:41,spd:51,matk:62,mdef:38},
    art:'👻', skills:['吸取','寄生种子','暗影波'], evolveTo:null, evolveLv:0,
    habitat:'弗瑞斯', dex:'飘荡的灵体,擅长魔法。'},
  // A阶
  劲帕奇:{tier:'A', type:'飞行', base:{hp:41,atk:66,def:39,spd:58,matk:63,mdef:50},
    art:'🦅', skills:['电击','电火花','雷之牙','击穿'], evolveTo:null, evolveLv:0,
    habitat:'天域', dex:'翱翔天域的电翼猛禽。'},
  仙人掌:{tier:'A', type:'格斗', base:{hp:42,atk:75,def:58,spd:40,matk:43,mdef:58},
    art:'🥷', skills:['空手切','反击','十字拳','格斗领域'], evolveTo:null, evolveLv:0,
    habitat:'远古墓地', dex:'仙人球的最终形态,格斗大师。'},
  凤凰花:{tier:'A', type:'火', base:{hp:45,atk:52,def:45,spd:39,matk:72,mdef:60},
    art:'🌻', skills:['藤鞭','摇摆','究极吸取','火焰之舞','浴火重生'], evolveTo:null, evolveLv:0,
    habitat:'太阳井', dex:'太阳井畔的不朽之花。'},
  // S阶
  双角巨龙:{tier:'S', type:'龙', base:{hp:56,atk:60,def:58,spd:51,matk:71,mdef:60},
    art:'🐲', skills:['石锤','挖洞','碎石','地之盟约','大地之力'], evolveTo:null, evolveLv:0,
    habitat:'神秘林', dex:'神秘林的远古龙裔,传说级存在。'},
  霹雳弹:{tier:'S', type:'火', base:{hp:30,atk:10,def:42,spd:22,matk:200,mdef:42},
    art:'💣', skills:['高压电击','十万伏特','百万伏特'], evolveTo:null, evolveLv:0,
    habitat:'虚无之地', dex:'魔力极致的禁忌造物。'},
  泰坦:{tier:'S', type:'普通', base:{hp:70,atk:69,def:63,spd:36,matk:53,mdef:45},
    art:'🗿', skills:['精神波动','能量护盾','思念头槌','刀枪不入'], evolveTo:null, evolveLv:0,
    habitat:'天堂城', dex:'天堂城的守护巨神。'}
};

// 关卡地图(魁星岛冒险路线)
const STAGES = [
  {name:'孢子林', wilds:['喇叭芽','小海螺','小蝙蝠'], lvRange:[1,3], boss:null},
  {name:'陨星湖', wilds:['小海螺','黑鱼','小蝙蝠'], lvRange:[3,5], boss:null},
  {name:'晶石山', wilds:['小蝙蝠','仙人球','黑鱼'], lvRange:[5,7], boss:null},
  {name:'神秘林', wilds:['小花蜗','小海螺','仙人球'], lvRange:[7,9], boss:{species:'双角巨龙', lv:10}},
  {name:'巨兽矿野', wilds:['仙人球','小花蜗'], lvRange:[9,11], boss:null},
  {name:'弗瑞斯', wilds:['魅力灵','小花蜗'], lvRange:[11,13], boss:null},
  {name:'赫里奥斯', wilds:['不死鸟','仙人球'], lvRange:[13,15], boss:null},
  {name:'太阳井', wilds:['凤凰花','不死鸟'], lvRange:[15,17], boss:{species:'凤凰花', lv:18}},
  {name:'天域', wilds:['劲帕奇','不死鸟'], lvRange:[17,19], boss:null},
  {name:'龙之首脊', wilds:['劲帕奇','魅力灵'], lvRange:[19,21], boss:{species:'双角巨龙', lv:22}}
];

/* ============ 游戏状态 ============ */
let G = null;

function defaultState(){
  // 初始队伍:喇叭芽 Lv1
  const starter = makeMon('喇叭芽', 1);
  return {
    stage:0, coins:100, balls:5, potions:2,
    team:[starter], active:0,
    cleared:new Array(STAGES.length).fill(false),
    dex:{}, // name -> true 已捕获
    dexSeen:{}
  };
}

function makeMon(speciesName, lv){
  const sp = SPECIES[speciesName];
  const b = sp.base;
  // 成长曲线:属性 = base * (1 + lv*0.12)
  const mult = 1 + (lv-1)*0.12;
  const mon = {
    species:speciesName, lv,
    type:sp.type, tier:sp.tier, art:sp.art,
    maxHp: Math.floor(b.hp * mult),
    hp: Math.floor(b.hp * mult),
    atk: Math.floor(b.atk * mult),
    def: Math.floor(b.def * mult),
    matk: Math.floor(b.matk * mult),
    mdef: Math.floor(b.mdef * mult),
    spd: Math.floor(b.spd * mult),
    exp:0, nextExp: 20 + lv*10,
    skills: sp.skills.map(n => ({name:n, ...SKILLS[n], maxpp:SKILLS[n].pp, pp:SKILLS[n].pp})),
    buffs:{def:0, matk:0}
  };
  return mon;
}

function rand(a,b){ return Math.floor(Math.random()*(b-a+1))+a; }
function clamp(v,a,b){ return Math.max(a,Math.min(b,v)); }

/* ============ 战斗逻辑 ============ */
let foe = null;
let inBattle = false;
let busy = false;

function calcDamage(attacker, defender, skill){
  const atkStat = skill.cat==='phys' ? attacker.atk + attacker.buffs.def*0 : attacker.matk + attacker.buffs.matk*0;
  const defStat = skill.cat==='phys' ? defender.def + defender.buffs.def : defender.mdef + defender.buffs.matk;
  const baseRatio = skill.cat==='phys'
    ? (attacker.atk / Math.max(1, defender.def))
    : (attacker.matk / Math.max(1, defender.mdef));
  let dmg = skill.power * baseRatio * 0.8;
  dmg *= typeMultiplier(skill.type, defender.type);
  dmg *= 0.85 + Math.random()*0.3; // 浮动
  return Math.max(1, Math.floor(dmg));
}

function logMsg(msg, cls=''){
  const el = document.getElementById('log');
  const p = document.createElement('p');
  p.className = cls;
  p.textContent = msg;
  el.appendChild(p);
  el.scrollTop = el.scrollHeight;
}

function renderAll(){
  renderMap();
  renderTeam();
  renderActive();
  renderFoe();
  renderSkills();
  document.getElementById('coins').textContent = G.coins;
  document.getElementById('balls').textContent = G.balls;
  document.getElementById('ballCount').textContent = G.balls;
  document.getElementById('potions').textContent = G.potions;
}

function renderMap(){
  document.getElementById('stageName').textContent = STAGES[G.stage].name;
  document.getElementById('stageIdx').textContent = G.stage+1;
  document.getElementById('stageTotal').textContent = STAGES.length;
  const bar = document.getElementById('mapBar');
  bar.innerHTML = '';
  STAGES.forEach((s,i)=>{
    const n = document.createElement('span');
    n.className = 'map-node' + (i===G.stage?' active':'') + (G.cleared[i]?' cleared':'') + (s.boss?' boss':'');
    n.textContent = (s.boss?'👑 ':'') + s.name;
    bar.appendChild(n);
  });
}

function renderTeam(){
  const strip = document.getElementById('teamStrip');
  strip.innerHTML = '';
  G.team.forEach((m,i)=>{
    const d = document.createElement('div');
    d.className = 'team-member' + (i===G.active?' active':'') + (m.hp<=0?' fainted':'');
    d.innerHTML = `${m.art} ${m.species}<br>Lv.${m.lv} ${m.hp>0?m.hp+'/'+m.maxHp:'倒下'}`;
    d.onclick = ()=>{ if(inBattle) return; if(m.hp>0){ G.active=i; save(); renderAll(); } };
    strip.appendChild(d);
  });
}

function renderActive(){
  const m = G.team[G.active];
  document.getElementById('myArt').textContent = m.art;
  document.getElementById('myName').textContent = m.species;
  document.getElementById('myLv').textContent = m.lv;
  document.getElementById('myType').textContent = m.type;
  document.getElementById('myType').style.background = TYPES[m.type].color;
  const ratio = m.hp/m.maxHp;
  const bar = document.getElementById('myHpBar');
  bar.style.width = (ratio*100)+'%';
  bar.className = 'hp-fill' + (ratio<0.3?' low':ratio<0.6?' mid':'');
  document.getElementById('myHp').textContent = m.hp;
  document.getElementById('myMaxHp').textContent = m.maxHp;
  document.getElementById('myExp').textContent = m.exp;
  document.getElementById('myNextExp').textContent = m.nextExp;
}

function renderFoe(){
  if(!foe){ return; }
  document.getElementById('foeArt').textContent = foe.art;
  document.getElementById('foeName').textContent = foe.isWild ? '野生 '+foe.species : foe.species;
  document.getElementById('foeLv').textContent = foe.lv;
  document.getElementById('foeType').textContent = foe.type;
  document.getElementById('foeType').style.background = TYPES[foe.type].color;
  const ratio = foe.hp/foe.maxHp;
  const bar = document.getElementById('foeHpBar');
  bar.style.width = (ratio*100)+'%';
  bar.className = 'hp-fill' + (ratio<0.3?' low':ratio<0.6?' mid':'');
  document.getElementById('foeHp').textContent = foe.hp;
  document.getElementById('foeMaxHp').textContent = foe.maxHp;
}

function renderSkills(){
  const m = G.team[G.active];
  const panel = document.getElementById('skillPanel');
  panel.innerHTML = '';
  m.skills.forEach((s,i)=>{
    const btn = document.createElement('button');
    btn.className = 'skill-btn';
    btn.disabled = busy || s.pp<=0 || !inBattle;
    btn.innerHTML = `<span class="cat ${s.cat}">${s.cat==='phys'?'物':s.cat==='magic'?'魔':'辅'}</span>${s.name} <span class="pp">PP ${s.pp}/${s.maxpp}</span>`;
    btn.title = s.desc;
    btn.onclick = ()=> useSkill(i);
    panel.appendChild(btn);
  });
}

/* ---- 遭遇战 ---- */
function startEncounter(){
  const stage = STAGES[G.stage];
  // 检查是否触发 BOSS
  if(stage.boss && !G.cleared[G.stage]){
    foe = makeMon(stage.boss.species, stage.boss.lv);
    foe.isWild = false;
  } else {
    const pool = stage.wilds;
    const spName = pool[rand(0,pool.length-1)];
    const lv = rand(stage.lvRange[0], stage.lvRange[1]);
    foe = makeMon(spName, lv);
    foe.isWild = true;
  }
  foe.hp = foe.maxHp;
  inBattle = true; busy = false;
  document.getElementById('nextBtn').style.display = 'none';
  // 记录图鉴
  G.dexSeen[foe.species] = true;
  save();
  logMsg(`⚔️ 遭遇 ${foe.isWild?'野生':''}${foe.species} Lv.${foe.lv}!`, 'sys');
  renderAll();
}

/* ---- 使用技能 ---- */
function useSkill(idx){
  if(busy || !inBattle) return;
  const me = G.team[G.active];
  const sk = me.skills[idx];
  if(sk.pp<=0){ logMsg('PP不足!', 'info'); return; }
  busy = true;
  renderSkills();
  sk.pp--;
  // 玩家行动
  if(sk.cat==='status'){
    if(sk.buff){
      if(sk.buff.def) { me.buffs.def += sk.buff.def; logMsg(`${me.species} 使用 ${sk.name},物防提升!`, 'heal'); }
      if(sk.buff.matk){ me.buffs.matk += sk.buff.matk; logMsg(`${me.species} 使用 ${sk.name},魔攻提升!`, 'heal'); }
    }
    if(sk.heal){
      const heal = Math.floor(me.maxHp * sk.heal);
      me.hp = clamp(me.hp+heal, 0, me.maxHp);
      logMsg(`${me.species} 使用 ${sk.name},恢复 ${heal} HP!`, 'heal');
    }
  } else {
    const dmg = calcDamage(me, foe, sk);
    const mult = typeMultiplier(sk.type, foe.type);
    foe.hp = clamp(foe.hp-dmg, 0, foe.maxHp);
    let eff = mult>1 ? ' 效果拔群!' : mult<1 ? ' 效果不佳…' : '';
    logMsg(`${me.species} 使用 ${sk.name},造成 ${dmg} 伤害${eff}`, 'dmg');
  }
  renderActive(); renderFoe();

  if(foe.hp<=0){ winBattle(); return; }

  // 敌方行动(速度判定)
  setTimeout(()=>{
    if(me.spd >= foe.spd){
      enemyTurn();
    } else {
      logMsg(`${foe.species} 速度更快,先发制人!`, 'info');
      enemyTurn();
    }
  }, 500);
}

function enemyTurn(){
  if(busy===false) {} // noop
  const me = G.team[G.active];
  // 敌方选技能:优先有PP的攻击技
  const atkSkills = foe.skills.filter(s=>s.cat!=='status' && s.pp>0);
  if(atkSkills.length===0){
    logMsg(`${foe.species} 无技可施!`, 'info');
    finishEnemyTurn();
    return;
  }
  const sk = atkSkills[rand(0,atkSkills.length-1)];
  sk.pp--;
  const dmg = calcDamage(foe, me, sk);
  const mult = typeMultiplier(sk.type, me.type);
  me.hp = clamp(me.hp-dmg, 0, me.maxHp);
  let eff = mult>1 ? ' 效果拔群!' : mult<1 ? ' 效果不佳…' : '';
  logMsg(`${foe.species} 使用 ${sk.name},对你造成 ${dmg} 伤害${eff}`, 'dmg');
  renderActive();
  if(me.hp<=0){
    logMsg(`💀 ${me.species} 倒下了!`, 'sys');
    // 切换到下一只
    const aliveIdx = G.team.findIndex(m=>m.hp>0);
    if(aliveIdx>=0){
      G.active = aliveIdx;
      logMsg(`👉 派出 ${G.team[aliveIdx].species}!`, 'info');
      renderAll();
      busy=false; renderSkills();
    } else {
      logMsg(`🚨 全队覆灭!逃回营地…`, 'sys');
      // 复活全队,回到当前关卡起点
      G.team.forEach(m=>{ m.hp = m.maxHp; });
      G.coins = Math.max(0, G.coins-20);
      inBattle=false; foe=null;
      renderAll();
    }
    return;
  }
  finishEnemyTurn();
}

function finishEnemyTurn(){
  busy=false;
  renderSkills();
  renderFoe();
}

/* ---- 胜利结算 ---- */
function winBattle(){
  inBattle = false;
  const reward = foe.isWild ? rand(8,15) : 50;
  G.coins += reward;
  logMsg(`🎉 击败了 ${foe.species}!获得 ${reward} G币`, 'sys');

  // 分配经验给全队
  const expGain = foe.isWild ? rand(10,18) : 60;
  G.team.forEach(m=>{
    if(m.hp>0){
      m.exp += expGain;
      logMsg(`${m.species} 获得 ${expGain} 经验`, 'info');
      while(m.exp >= m.nextExp){
        m.exp -= m.nextExp;
        m.lv++;
        m.maxHp += Math.floor(SPECIES[m.species].base.hp*0.12);
        m.hp = m.maxHp;
        m.atk += Math.floor(SPECIES[m.species].base.atk*0.12);
        m.def += Math.floor(SPECIES[m.species].base.def*0.12);
        m.matk += Math.floor(SPECIES[m.species].base.matk*0.12);
        m.mdef += Math.floor(SPECIES[m.species].base.mdef*0.12);
        m.spd += Math.floor(SPECIES[m.species].base.spd*0.12);
        m.nextExp = 20 + m.lv*10;
        logMsg(`✨ ${m.species} 升到 Lv.${m.lv}!`, 'heal');
        // 进化判定
        const sp = SPECIES[m.species];
        if(sp.evolveTo && m.lv >= sp.evolveLv){
          const oldName = m.species;
          const evolved = makeMon(sp.evolveTo, m.lv);
          evolved.exp = m.exp;
          const idx = G.team.indexOf(m);
          G.team[idx] = evolved;
          if(G.active===idx) G.active=idx;
          logMsg(`🌟 ${oldName} 进化为 ${sp.evolveTo}!`, 'sys');
        }
      }
    }
  });

  // 标记关卡清除

Game Source: 怪物X联盟 · 单机版

Creator: TurboBolt40

Libraries: none

Complexity: complex (637 lines, 26.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: x-turbobolt40" to link back to the original. Then publish at arcadelab.ai/publish.