Git游戏

最新动态:
查看全部
每日签到 ×
0
连签天数
0
累签次数
0
今日已签到

签到规则

每日签到获得 5 积分

连签达标可额外获得奖励:

最新动态
您现在的位置是:首页 > 攻略大全 > 辅助脚本

辅助脚本

birb存档编辑器

2026-09-22 辅助脚本8 作者:小镇骑士 投稿者:小镇骑士
本文作者为 @小镇骑士。
将一下内容保存为 HTML 格式文件并打开即可使用。如:birb存档编辑器.html<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Birb ...

游戏介绍:

https://g8hh.com.cn/#/game/birb

将一下内容保存为 HTML 格式文件并打开即可使用。如:birb存档编辑器.html


<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Birb 存档编辑器</title>

<style>

/* SECTION: base */

:root{

  --bg:#171210; --panel:#221a15; --panel2:#2b211a; --line:#3d2f24;

  --text:#f3e6d0; --dim:#a8937a; --gold:#ffd24a; --gold-deep:#e8a530;

  --green:#8fd168; --red:#ff7a6b; --blue:#7ec8e3;

}

*{box-sizing:border-box;margin:0;padding:0}

body{background:var(--bg);color:var(--text);font-family:"Microsoft YaHei","PingFang SC",sans-serif;font-size:14px;line-height:1.6;padding:0 0 60px}

h1,h2,h3{font-weight:700;letter-spacing:.5px}

button,input,select,textarea{font-family:inherit;font-size:inherit;color:inherit}

button{cursor:pointer;border:none;border-radius:6px;padding:8px 16px;background:var(--panel2);color:var(--text);border:1px solid var(--line);transition:background .15s,transform .05s}

button:hover{background:#3a2c20}

button:active{transform:translateY(1px)}

button.primary{background:var(--gold);color:#2a1c05;font-weight:700;border:none}

button.primary:hover{background:#ffdd6e}

button.danger{background:#5a221d;color:#ffb4ab;border-color:#7c332b}

button.danger:hover{background:#6e2a23}

button:disabled{opacity:.45;cursor:not-allowed}

input[type=number],input[type=text],select,textarea{background:#181009;border:1px solid var(--line);border-radius:6px;padding:6px 10px;color:var(--text);width:100%}

input:focus,select:focus,textarea:focus{outline:2px solid var(--gold-deep);outline-offset:0}

input.dirty{border-color:var(--gold);box-shadow:0 0 0 1px var(--gold)}

.mono{font-family:Consolas,Menlo,monospace}

/* SECTION: layout */

header{background:linear-gradient(180deg,#241a12,#171210);border-bottom:2px solid var(--line);padding:18px 20px;text-align:center}

header h1{color:var(--gold);font-size:24px;text-shadow:2px 2px 0 #000}

header p{color:var(--dim);font-size:12px;margin-top:4px}

.wrap{max-width:1080px;margin:0 auto;padding:16px}

.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:14px}

.card h2{font-size:15px;color:var(--gold);margin-bottom:10px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}

.card h2 .sub{font-size:12px;color:var(--dim);font-weight:400}

.row{display:flex;gap:10px;flex-wrap:wrap}

.row>button{flex:0 0 auto}

.field-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}

.field{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:8px 10px}

.field label{display:block;font-size:12px;color:var(--dim);margin-bottom:4px}

.field .eng{color:#6d5c49;font-size:11px}

.flag-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:6px}

.flag-item{display:flex;align-items:center;justify-content:space-between;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:6px 10px;font-size:13px}

.flag-item.dirty{border-color:var(--gold)}

.flag-item code{font-size:10px;color:#6d5c49;display:block}

/* SECTION: switch */

.switch{position:relative;width:40px;height:22px;flex:0 0 40px}

.switch input{opacity:0;width:0;height:0}

.switch .track{position:absolute;inset:0;background:#3a2c20;border:1px solid var(--line);border-radius:12px;transition:.15s}

.switch .track::after{content:"";position:absolute;left:2px;top:2px;width:16px;height:16px;border-radius:50%;background:#7a6a55;transition:.15s}

.switch input:checked+.track{background:var(--gold-deep);border-color:var(--gold)}

.switch input:checked+.track::after{left:20px;background:#fff3c8}

/* SECTION: tabs */

.tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}

.tabs button{padding:7px 14px;font-size:13px;border-radius:20px}

.tabs button.active{background:var(--gold);color:#2a1c05;font-weight:700;border-color:var(--gold)}

.tab-panel{display:none}

.tab-panel.active{display:block}

/* SECTION: status */

.status{padding:10px 12px;border-radius:8px;font-size:13px;margin-top:10px;display:none}

.status.show{display:block}

.status.ok{background:#1e3320;border:1px solid #3a6b3e;color:var(--green)}

.status.err{background:#3a1815;border:1px solid #7c332b;color:var(--red)}

.status.info{background:#152433;border:1px solid #2e5a7c;color:var(--blue)}

.summary-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:10px}

.summary-item{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:8px 10px;text-align:center}

.summary-item .v{font-size:18px;color:var(--gold);font-weight:700;word-break:break-all}

.summary-item .k{font-size:11px;color:var(--dim)}

.badge{display:inline-block;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 8px;font-size:11px;color:var(--dim)}

.dirty-count{color:var(--gold);font-weight:700}

textarea#raw-json{width:100%;min-height:340px;resize:vertical;font-size:12px;line-height:1.5}

textarea#paste-input{width:100%;min-height:90px;resize:vertical;font-size:12px}

.file-drop{border:2px dashed var(--line);border-radius:10px;padding:18px;text-align:center;color:var(--dim);cursor:pointer;transition:.15s}

.file-drop:hover,.file-drop.drag{border-color:var(--gold);color:var(--gold);background:#241a12}

.hint{font-size:12px;color:var(--dim);margin-top:8px}

.preset-bar{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}

.hidden{display:none!important}

@media(max-width:640px){

  .field-grid,.flag-list{grid-template-columns:1fr}

  header h1{font-size:19px}

}

</style>

</head>

<body>


<header>

  <h1>Birb 存档编辑器</h1>

  <p>导入 .birb 存档 → 修改资源 / 进度 / 伙伴 → 导出回游戏(格式与游戏原生导出完全一致)</p>

</header>


<div>


<!-- SECTION: import -->

<div id="card-import">

  <h2>① 导入存档</h2>

  <div id="file-drop">

    点击选择 <b>.birb</b> 存档文件,或将文件拖到这里

    <input type="file" id="file-input" accept=".birb,.txt,.dat" hidden>

  </div>

  <div>也可以直接粘贴存档文本(导出的编码字符串或原始 JSON):</div>

  <textarea id="paste-input" placeholder="粘贴存档内容后点击右侧「解码」…"></textarea>

  <div style="margin-top:8px">

    <button id="btn-decode" data-primary-action>解码</button>

  </div>

  <div id="import-status"></div>

</div>


<!-- SECTION: editor (hidden until loaded) -->

<div id="editor-area">


  <!-- SECTION: summary -->

  <div>

    <h2>② 存档概要 <span id="sum-version"></span> <span>修改 <span id="dirty-count">0</span> 项</span></h2>

    <div id="summary-grid"></div>

    <div>

      <button id="preset-resources">一键资源拉满</button>

      <button id="preset-unlock">一键解锁系统</button>

      <button id="preset-tutorials">一键跳过全部教程</button>

      <button id="btn-reimport">放弃修改,重新导入</button>

    </div>

  </div>


  <!-- SECTION: tabs -->

  <div id="tabs"></div>


  <div class="card tab-panel" id="panel-form"></div>

  <div class="card tab-panel" id="panel-flags"></div>


  <!-- SECTION: raw json -->

  <div class="card tab-panel" id="panel-json">

    <h2>原始 JSON 编辑 <span>高级:直接修改完整结构,应用后与表单双向同步</span></h2>

    <textarea id="raw-json" spellcheck="false"></textarea>

    <div style="margin-top:8px">

      <button id="btn-apply-json">应用 JSON 修改</button>

      <button id="btn-pretty-json">从当前状态刷新</button>

    </div>

    <div id="json-status"></div>

  </div>


  <!-- SECTION: export -->

  <div>

    <h2>③ 导出存档</h2>

    <div style="margin:0 0 10px">导出文件与游戏内「导出存档(EXPORT SAVE)」格式一致,可在游戏「导入存档(IMPORT SAVE)」中直接使用。游戏导入前会自动备份你的原存档。</div>

    <div>

      <button id="btn-download" data-primary-action>下载 .birb 文件</button>

      <button id="btn-copy">复制编码文本</button>

    </div>

    <div id="export-status"></div>

  </div>

</div>


<!-- SECTION: empty state -->

<div id="empty-state">

  <div style="text-align:center;color:var(--dim);padding:26px 10px">

    <div style="font-size:34px">?</div>

    <p style="margin-top:8px">还没有导入存档。导入后即可编辑资源、伙伴、进度等全部数据。</p>

  </div>

</div>


</div>


<script>

/* SECTION: codec —— 与游戏源码一致的编解码(Base64 + XOR,密钥来自游戏源码) */

const XOR_KEY = 'BiRb_P0pC0rn_2024!';

const KEY_BYTES = new TextEncoder().encode(XOR_KEY);

const CHUNK = 0x8000;


function xorBytes(bytes){

  const out = new Uint8Array(bytes.length);

  for(let i=0;i<bytes.length;i++) out[i] = bytes[i] ^ KEY_BYTES[i % KEY_BYTES.length];

  return out;

}

function bytesToBase64(bytes){

  let s = '';

  for(let i=0;i<bytes.length;i+=CHUNK){

    s += String.fromCharCode.apply(null, bytes.subarray(i, i+CHUNK));

  }

  return btoa(s);

}

function base64ToBytes(b64){

  const bin = atob(b64);

  const bytes = new Uint8Array(bin.length);

  for(let i=0;i<bin.length;i++) bytes[i] = bin.charCodeAt(i);

  return bytes;

}

function encodeSave(json){

  return bytesToBase64(xorBytes(new TextEncoder().encode(json)));

}

function decodeSave(raw){

  const clean = String(raw).trim();

  if(!clean) throw new Error('内容为空');

  let bytes;

  try { bytes = base64ToBytes(clean); }

  catch(e){ throw new Error('不是有效的 Base64 编码'); }

  const plain = new TextDecoder('utf-8', {fatal:false}).decode(xorBytes(bytes));

  if(plain.includes('\uFFFD')){

    // 与游戏相同的兼容回退:对 Base64 解码后的字符按 16 位异或解码

    const bin = atob(clean);

    const u16 = new Uint16Array(bin.length);

    for(let i=0;i<bin.length;i++) u16[i] = bin.charCodeAt(i) ^ XOR_KEY.charCodeAt(i % XOR_KEY.length);

    let s = '';

    for(let i=0;i<u16.length;i+=CHUNK) s += String.fromCharCode.apply(null, u16.subarray(i, i+CHUNK));

    return s;

  }

  return plain;

}

function parseAny(raw){

  // 支持:编码存档 / 原始 JSON

  let json = null;

  const trimmed = String(raw).trim();

  if(trimmed.startsWith('{')){ json = trimmed; }

  else{

    try { json = decodeSave(trimmed); }

    catch(e){ throw new Error('解码失败:' + e.message + '(若不是编码存档,请粘贴以 { 开头的原始 JSON)'); }

  }

  let obj;

  try { obj = JSON.parse(json); }

  catch(e){ throw new Error('JSON 解析失败:' + e.message); }

  if(typeof obj !== 'object' || obj === null || Array.isArray(obj)) throw new Error('存档结构无效:顶层应为对象');

  return obj;

}


/* SECTION: state */

let state = null;          // 当前内存中的存档对象

let originalRaw = null;    // 最近一次导入的原文

const dirty = new Set();   // 被修改过的字段路径


function getPath(obj, path){

  return path.split('.').reduce((o,k)=> (o==null ? undefined : o[k]), obj);

}

function setPath(obj, path, val){

  const ks = path.split('.');

  let o = obj;

  for(let i=0;i<ks.length-1;i++){

    if(typeof o[ks[i]] !== 'object' || o[ks[i]] === null) o[ks[i]] = {};

    o = o[ks[i]];

  }

  o[ks[ks.length-1]] = val;

}

function markDirty(path){

  dirty.add(path);

  document.querySelectorAll('[data-path="'+CSS.escape(path)+'"]').forEach(el=>el.classList.add('dirty'));

  updateDirtyCount();

}

function updateDirtyCount(){

  document.getElementById('dirty-count').textContent = dirty.size;

}

function clearDirty(){

  dirty.clear();

  document.querySelectorAll('.dirty').forEach(el=>el.classList.remove('dirty'));

  updateDirtyCount();

}


/* SECTION: field schema —— 字段定义(标签对照游戏中文语言包) */

const GROUPS = [

  { id:'resources', name:'? 资源', fields:[

    ['resources.popcorn','爆米花','int'],

    ['resources.goldenPopcorn','金色爆米花','int'],

    ['resources.goldenFeathers','金色羽毛','int'],

    ['resources.sunflowerSeeds','种子','int'],

    ['resources.monetariaMoneta','莫奈塔','int'],

    ['resources.twigs','树枝','int'],

    ['resources.bruteOre','原矿','int'],

    ['resources.oreBars','矿锭','int'],

    ['resources.deepCores','深层核心','int'],

  ]},

  { id:'player', name:'? 角色属性', fields:[

    ['player.x','X 坐标','float'],

    ['player.y','Y 坐标','float'],

    ['player.speed','移动速度','float'],

    ['player.pickupRadius','拾取半径','float'],

    ['player.customization.hue','外观色相','int'],

    ['player.customization.saturation','外观饱和度','int'],

    ['player.customization.brightness','外观亮度','int'],

    ['player.customization.collarHue','项圈色相','int'],

  ]},

  { id:'progress', name:'? 进度与统计', fields:[

    ['totalPopcornCollected','累计爆米花','int'],

    ['playTime','游戏时长(原始值)','int'],

    ['evolutionCount','进化次数','int'],

    ['rebirthCount','转生次数','int'],

    ['totalFishCaught','累计捕获鱼','int'],

    ['manualFishingCatches','手动钓鱼捕获数','int'],

    ['arcadeMatchesPlayed','街机比赛场次','int'],

    ['monsterFeedProgress','怪物喂食进度','float'],

    ['monsterPopcornFed','喂给怪物的爆米花','int'],

    ['monsterFeathersFed','喂给怪物的羽毛','int'],

    ['monsterSeedsFed','喂给怪物的种子','int'],

    ['sparrowTotalEnergyDrained','麻雀累计吸取能量','int'],

    ['sparrowPrestigeCount','麻雀威望次数','int'],

    ['sparrowResonanceXP','麻雀共鸣经验','int'],

    ['lastShinyFishCaughtAt','上次捕获闪光鱼时间戳','int'],

  ]},

  { id:'sparrow', name:'? 麻雀', fields:[

    ['sparrow.level','等级','int'],

    ['sparrow.xp','经验','int'],

    ['sparrow.efficiencyBonus','效率加成','float'],

    ['sparrow.maxLevelReached','达到过的最高等级','int'],

  ], flags:[

    ['sparrow.unlocked','已解锁'],

  ]},

  { id:'seagull', name:'?️ 海鸥', fields:[

    ['seagull.level','等级','int'],

    ['seagull.xp','经验','int'],

    ['seagull.migrationCount','迁徙次数','int'],

    ['seagull.dailyLegendaryCycle','每日传说循环','int'],

  ], flags:[

    ['seagull.discovered','已发现'],

    ['seagull.hasReborn','已转生'],

    ['seagull.autoRebirbEnabled','自动转生'],

    ['hasMetSeagull','已遇见海鸥'],

  ]},

  { id:'parrot', name:'? 鹦鹉', fields:[

    ['parrot.level','等级','int'],

    ['parrot.xp','经验','int'],

    ['parrot.skillPoints','技能点','int'],

    ['parrot.rebirbCount','转生次数','int'],

    ['parrot.maxInventorySize','背包容量','int'],

    ['parrot.skills.hp','技能:生命','int'],

    ['parrot.skills.lifeRegen','技能:生命回复','int'],

    ['parrot.skills.damage','技能:伤害','int'],

    ['parrot.autoPotionHealthThreshold','自动喝药血量阈值','int'],

  ], flags:[

    ['parrot.unlocked','已解锁'],

    ['parrot.autoPotionEnabled','自动喝药'],

    ['parrot.autoDiscardDuplicateArtifacts','自动丢弃重复神器'],

    ['hasSeenBirbsmithIntro','已见鹦鹉锻造介绍'],

    ['hasSeenParrotForgeTutorial','已见鹦鹉熔炉教程'],

    ['hasSeenCrowMetalworkTutorial','已见乌鸦金工教程'],

  ]},

  { id:'dove', name:'? 环颈斑鸠', fields:[

    ['collaredDove.unspentRebirbPoints','未分配转生点','int'],

    ['collaredDove.charge','充能','int'],

    ['collaredDove.level','等级','int'],

    ['collaredDove.xp','经验','int'],

    ['collaredDove.seedTrainingLevel','种子训练等级','int'],

    ['collaredDove.skillPoints','技能点','int'],

    ['collaredDove.rebirbCount','转生次数','int'],

    ['collaredDove.instincts.scavenger','本能:拾荒','int'],

    ['collaredDove.instincts.sweep','本能:扫食','int'],

    ['collaredDove.instincts.gilded','本能:鎏金','int'],

    ['collaredDove.lifetimeSweeps','累计扫食次数','int'],

    ['collaredDove.lifetimePopcornCollected','累计收集爆米花','int'],

    ['collaredDove.lifetimeGoldenCollected','累计收集金色爆米花','int'],

  ]},

  { id:'panda', name:'? 小熊猫', fields:[

    ['redPanda.name','名字','text'],

    ['redPanda.tier','阶级','int'],

    ['redPanda.mode','模式(chill 等)','text'],

  ], flags:[

    ['redPanda.named','已命名'],

    ['redPanda.introSeen','已见介绍'],

  ]},

  { id:'fishing', name:'? 钓鱼', fields:[

    ['fishing.level','钓鱼等级','int'],

    ['fishing.xp','钓鱼经验','int'],

    ['aquarium.market.contractReputationXp','水族馆契约声望经验','int'],

    ['fishBuffThirdSlotUnlocked','鱼 Buff 第三槽','int'],

  ], flags:[

    ['isAutoFishing','自动钓鱼中'],

    ['hasSeenFishingUnlockTutorial','已见钓鱼解锁教程'],

    ['hasSeenFishingBridgeTutorial','已见钓鱼桥教程'],

    ['hasSeenAquariumUnlockTutorial','已见水族馆解锁教程'],

    ['hasSeenAquariumTutorial','已见水族馆教程'],

    ['hasSeenFishMarketTutorial','已见鱼市教程'],

    ['hasSeenSparrowFishingUnlockTutorial','已见麻雀钓鱼教程'],

  ]},

  { id:'expedition', name:'⚔️ 探险(远征)', fields:[

    ['expedition.progress.level','等级','int'],

    ['expedition.progress.xp','经验','int'],

    ['expedition.progress.xpToNextLevel','升级所需经验','int'],

    ['expedition.progress.totalXpEarned','累计获得经验','int'],

    ['expedition.totalRuns','总运行次数','int'],

    ['expedition.successfulRuns','成功次数','int'],

    ['expedition.totalEnemiesDefeated','累计击败敌人','int'],

    ['expedition.bestSurvivalTime','最佳生存时间','int'],

    ['expedition.highestWaveReached','最高波次','int'],

    ['expedition.highestFloorReached','最高层数','int'],

    ['expedition.totalResourcesCollected.twigs','探险累计:树枝','int'],

    ['expedition.totalResourcesCollected.seeds','探险累计:种子','int'],

    ['expedition.totalResourcesCollected.shinies','探险累计:闪光物','int'],

    ['expedition.totalResourcesCollected.chests','探险累计:普通宝箱','int'],

    ['expedition.totalResourcesCollected.uncommonChests','探险累计:稀有宝箱','int'],

    ['expedition.totalResourcesCollected.rareChests','探险累计:珍贵宝箱','int'],

    ['expedition.totalResourcesCollected.epicChests','探险累计:史诗宝箱','int'],

    ['expedition.totalResourcesCollected.legendaryChests','探险累计:传说宝箱','int'],

    ['expedition.equipmentChests.common','装备宝箱:普通','int'],

    ['expedition.equipmentChests.uncommon','装备宝箱:稀有','int'],

    ['expedition.equipmentChests.rare','装备宝箱:珍贵','int'],

    ['expedition.equipmentChests.epic','装备宝箱:史诗','int'],

    ['expedition.equipmentChests.legendary','装备宝箱:传说','int'],

    ['expedition.sacrifice.skillPointsSacrificed','献祭技能点','int'],

    ['expedition.sacrifice.multiplierBonus','献祭倍率加成','float'],

    ['expedition.sacrifice.seedMultiplierBonus','种子倍率加成','float'],

    ['expedition.sacrifice.chestChanceBonus','宝箱概率加成','float'],

  ], flags:[

    ['expedition.unlocked','探险已解锁'],

    ['expedition.isAutoAttack','自动攻击'],

    ['hasEnteredDungeon','已进入地牢'],

    ['hasTalkedToMonster','已与怪物对话'],

    ['hasMetMonsterInExpedition','已在探险中遇见怪物'],

    ['floorOneMineEntranceOpened','一层矿洞入口已开'],

  ]},

  { id:'nest', name:'? 巢穴', fields:[

    ['nest.tier','巢穴等级','int'],

    ['nest.resources.twigs','巢穴树枝','int'],

    ['nest.resources.wood','木材','int'],

    ['nest.nestEssence','巢穴精华','int'],

    ['nest.totalEssenceEarned','累计精华','int'],

    ['nest.migrationCount','迁徙次数','int'],

    ['nest.perks.attackBonus','天赋:攻击加成','int'],

    ['nest.perks.healthBonus','天赋:生命加成','int'],

    ['nest.perks.resourceBonus','天赋:资源加成','int'],

  ], flags:[

    ['nest.unlocked','巢穴已解锁'],

    ['nest.autoPopcornEnabled','自动爆米花'],

    ['nest.autoPrestigeEnabled','自动威望'],

    ['nest.autoSeedsEnabled','自动种子'],

  ]},

  { id:'mine', name:'⛏️ 矿洞', fields:[

    ['mine.milestoneLevel','里程碑等级','int'],

    ['mine.crowLevel','乌鸦等级','int'],

    ['mine.crowXp','乌鸦经验','int'],

    ['mine.crowArmorLevel','乌鸦护甲等级','int'],

    ['mine.crowArmorXp','乌鸦护甲经验','int'],

    ['mine.crowDamageLevel','乌鸦伤害等级','int'],

    ['mine.crowDamageXp','乌鸦伤害经验','int'],

    ['mine.crowRegenLevel','乌鸦回复等级','int'],

    ['mine.crowRegenXp','乌鸦回复经验','int'],

    ['mine.totalCollapses','累计坍塌次数','int'],

    ['mine.runBruteOreEarned','本轮获得原矿','int'],

    ['mine.armorDrainRatePercent','护甲损耗率%','int'],

  ], flags:[

    ['mine.autoCollectEnabled','自动收集'],

    ['hasSeenMineTutorial','已见矿洞教程'],

  ]},

  { id:'hats', name:'? 帽子解锁进度', fields:[

    ['hatUnlockProgress.bestBridgeCrossingMs','最快过桥(毫秒)','int'],

    ['hatUnlockProgress.maxSimultaneousShinyBuffs','最多同时闪光 Buff','int'],

    ['hatUnlockProgress.totalGoldenPopcornCollected','累计金色爆米花','int'],

    ['hatUnlockProgress.totalSunflowerSeedsGained','累计获得种子','int'],

    ['hatUnlockProgress.arcadeWins','街机胜场','int'],

    ['hatUnlockProgress.legendaryPopcornFishEaten','吃掉的传说爆米花鱼','int'],

    ['hatUnlockProgress.maxPopcornOwned','拥有过的最多爆米花','int'],

  ], flags:[

    ['hatUnlockProgress.sunflowerVendingUnlocked','向日葵自动售货机已解锁'],

    ['hatUnlockProgress.playedOnSteamReleaseDay','曾在 Steam 发布日游玩'],

  ]},

  { id:'settings', name:'⚙️ 设置', fields:[

    ['settings.language','语言','text'],

    ['settings.audio.musicVolume','音乐音量(0~1)','float'],

    ['settings.audio.sfxVolume','音效音量(0~1)','float'],

    ['settings.fpsLimit','FPS 上限(0=无限制)','int'],

    ['settings.uiScale','界面缩放','float'],

    ['settings.fishingUiOpacity','钓鱼界面透明度','float'],

  ], flags:[

    ['settings.audio.musicEnabled','音乐开启'],

    ['settings.audio.sfxEnabled','音效开启'],

    ['settings.useScientific','科学计数法'],

    ['settings.disableChat','禁用聊天'],

    ['settings.disableCustomCursor','禁用自定义光标'],

    ['settings.disableFloatTextNotifications','禁用浮动文字通知'],

    ['settings.hideFishCaughtNotification','隐藏捕到鱼通知'],

    ['settings.hideParrotAggroRadius','隐藏鹦鹉仇恨半径'],

  ]},

];


/* SECTION: render tabs & form */

const tabsEl = document.getElementById('tabs');

const panelForm = document.getElementById('panel-form');

const panelFlags = document.getElementById('panel-flags');

const panelJson = document.getElementById('panel-json');


function buildTabs(){

  tabsEl.innerHTML = '';

  const mk = (id, label) => {

    const b = document.createElement('button');

    b.textContent = label; b.dataset.tab = id;

    b.onclick = () => activateTab(id);

    tabsEl.appendChild(b);

  };

  mk('form', '? 分类编辑');

  mk('flags', '? 标记与教程');

  mk('json', '? 原始 JSON');

}

function activateTab(id){

  document.querySelectorAll('.tabs button').forEach(b=>b.classList.toggle('active', b.dataset.tab===id));

  panelForm.classList.toggle('active', id==='form');

  panelFlags.classList.toggle('active', id==='flags');

  panelJson.classList.toggle('active', id==='json');

  if(id==='json') document.getElementById('raw-json').value = JSON.stringify(state, null, 2);

}


function sanitizeNumber(v, type){

  let n = Number(v);

  if(!Number.isFinite(n)) return 0;

  if(n < 0) n = 0;

  return type==='int' ? Math.floor(n) : n;

}


function renderForm(){

  panelForm.innerHTML = '';

  const h = document.createElement('h2');

  h.innerHTML = '分类字段编辑 <span>修改后记得导出;金色边框 = 已修改未导出</span>';

  panelForm.appendChild(h);


  GROUPS.forEach(group => {

    const sec = document.createElement('div');

    sec.style.marginBottom = '18px';

    const gh = document.createElement('h3');

    gh.textContent = group.name;

    gh.style.cssText = 'font-size:14px;margin-bottom:8px;color:var(--gold-deep)';

    sec.appendChild(gh);

    const grid = document.createElement('div');

    grid.className = 'field-grid';


    const makeField = (path, label, type) => {

      const cur = getPath(state, path);

      const div = document.createElement('div');

      div.className = 'field'; div.dataset.path = path;

      if(dirty.has(path)) div.classList.add('dirty');

      const lab = document.createElement('label');

      const eng = path.split('.').pop();

      lab.innerHTML = label + (eng !== label ? ' <span class="eng mono">'+eng+'</span>' : '');

      div.appendChild(lab);


      if(type === 'bool'){

        const sw = document.createElement('label');

        sw.className = 'switch';

        const inp = document.createElement('input');

        inp.type = 'checkbox'; inp.checked = !!cur;

        const tr = document.createElement('span'); tr.className='track';

        sw.append(inp, tr);

        inp.onchange = () => { setPath(state, path, inp.checked); markDirty(path); };

        div.appendChild(sw);

      } else if(type === 'text'){

        const inp = document.createElement('input');

        inp.type = 'text'; inp.value = cur == null ? '' : String(cur);

        inp.onchange = () => { setPath(state, path, inp.value); markDirty(path); };

        div.appendChild(inp);

      } else {

        const inp = document.createElement('input');

        inp.type = 'number'; inp.min = '0';

        inp.step = type==='int' ? '1' : 'any';

        inp.value = cur == null ? 0 : cur;

        inp.onchange = () => {

          const n = sanitizeNumber(inp.value, type);

          inp.value = n;

          setPath(state, path, n); markDirty(path);

        };

        div.appendChild(inp);

      }

      grid.appendChild(div);

    };


    (group.flags||[]).forEach(f => makeField(f[0], f[1], 'bool'));

    group.fields.forEach(f => makeField(f[0], f[1], f[2]));

    sec.appendChild(grid);

    panelForm.appendChild(sec);

  });

}


/* SECTION: flags tab —— 自动收集顶层布尔标记 */

function renderFlags(){

  panelFlags.innerHTML = '';

  const h = document.createElement('h2');

  h.innerHTML = '标记与教程开关 <span>自动列出的存档顶层布尔字段(教程、剧情、解锁标记等)</span>';

  panelFlags.appendChild(h);

  const all = document.createElement('div');

  all.className = 'row'; all.style.marginBottom = '10px';

  const bAll = document.createElement('button');

  bAll.textContent = '全部打开';

  const bNone = document.createElement('button');

  bNone.textContent = '全部关闭';

  const list = document.createElement('d

0.0
暂无评分
给这篇文章打分
满分10分,点击星星打分
评分分布
5星
0%
4星
0%
3星
0%
2星
0%
1星
0%

文章评论

共有0条评论来说两句吧...

每日游戏盲盒
点击盲盒,开出今日惊喜游戏
游戏库
最近0 收藏0 游玩中0 已通关0
加载中...
最近在玩
为「birb存档编辑器」补充标签
你推荐的标签将在管理员审核通过后展示在页面的标签区,便于大家找到这篇文章。每行一个,不要带标点符号。审核通过后同时计入你的「补充标签」任务进度。
每行一个标签,最多 10 个,提交后等待管理员审核展示