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 ...
游戏介绍:
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











微信打赏支持
支付宝打赏支持