(() => {
  "use strict";
  const assetVersion="mainnet-play-23";
  const dashboardStyles=document.createElement("link"); dashboardStyles.rel="stylesheet"; dashboardStyles.href="player-dashboard.css?v="+assetVersion; document.head.appendChild(dashboardStyles);
  const hiddenStateStyles=document.createElement("style"); hiddenStateStyles.textContent=".being-view[hidden]{display:none!important}"; document.head.appendChild(hiddenStateStyles);
  const cfg = window.ETERNAL_BEINGS_INSTANCE;
  const $ = (id) => document.getElementById(id);
  const zero = "0x" + "00".repeat(32);
  const eventLogChunkBlocks = 10000;
  const abi = [
    "function startBlock() view returns (uint256)",
    "function currentEpoch() view returns (uint256)",
    "function epochStart(uint256) view returns (uint256)",
    "function EPOCH_BLOCKS() view returns (uint256)",
    "function COMMIT_BLOCKS() view returns (uint256)",
    "function MINTS_PER_EPOCH() view returns (uint256)",
    "function MAX_BEINGS() view returns (uint256)",
    "function totalMinted() view returns (uint256)",
    "function aliveSupply() view returns (uint256)",
    "function releasedMintCap(uint256) view returns (uint256)",
    "function revealedCount(uint256) view returns (uint32)",
    "function epochClaimLimit(uint256) view returns (uint32)",
    "function epochClaimedCount(uint256) view returns (uint32)",
    "function commitments(uint256,address) view returns (bytes32)",
    "function revealed(uint256,address) view returns (bool)",
    "function claimedEpoch(uint256,address) view returns (bool)",
    "function hasMintedBeing(address) view returns (bool)",
    "function hasRevealedAny(address) view returns (bool)",
    "function epochSeed(uint256) view returns (bytes32)",
    "function balanceOf(address) view returns (uint256)",
    "function ownerOf(uint256) view returns (address)",
    "function cooldownUntil(uint256) view returns (uint256)",
    "function COOLDOWN_BLOCKS() view returns (uint256)",
    "function MAX_ENDURANCE() view returns (uint256)",
    "function UNKNOWN_HOLD_BLOCKS() view returns (uint256)",
    "function CRYPTOPUNK_OBSERVATION_BLOCKS() view returns (uint256)",
    "function UNKNOWN_MIN_TOTAL_SUPPLY() view returns (uint256)",
    "function UNKNOWN_COLLECTION_DEVOUR_LIMIT() view returns (uint256)",
    "function topCollectionsRoot() view returns (bytes32)",
    "function ore() view returns (address)",
    "function renderer() view returns (address)",
    "function externalObservations(address,uint256) view returns (address owner,uint64 blockNumber,bytes32 codehash)",
    "function cryptoPunkDeposits(address,uint256) view returns (address owner,uint64 blockNumber,uint8 tier,bytes32 codehash)",
    "function devouredExternal(address,uint256) view returns (bool)",
    "function unknownCollectionDevours(address) view returns (uint32)",
    "function verifyCollectionTier(address,uint8,bytes32[]) view returns (bool)",
    "function nutritionForTier(uint8) view returns (uint32 nutrition,uint32 complexityGain)",
    "function hunts(uint256) view returns (address owner,uint64 startBlock,uint64 endurance,uint32 difficulty,uint16 sceneId,uint16 tokenMultiplier,uint16 powerRate,uint16 skillRate,bytes32 seed)",
    "function getBeing(uint256) view returns (tuple(uint128 mass,uint128 complexity,uint64 devours,uint64 fusions,uint64 premiumDevours,uint64 markLuck,uint64 huntNonce,uint32 power,uint32 skill,uint32 scars,uint16 stage,uint8 originClass,uint8 originSymbol,uint8 mutationBias,uint8 lineageMask,bytes32 genome))",
    "function tokenURI(uint256) view returns (string)",
    "event BeingMinted(address indexed user,uint256 indexed tokenId,bytes32 genome)",
    "event Transfer(address indexed from,address indexed to,uint256 indexed tokenId)",
    "event Committed(uint256 indexed epoch,address indexed user,bytes32 commitment)",
    "event Revealed(uint256 indexed epoch,address indexed user)",
    "event HuntEntered(uint256 indexed tokenId,address indexed owner,uint16 sceneId,uint32 difficulty,uint64 endurance)",
    "event HuntResolved(uint256 indexed tokenId,address indexed owner,uint256 reward,bool powerUp,bool skillUp)",
    "function commitMint(bytes32)",
    "function revealMint(uint256,bytes32)",
    "function claimMint(uint256)",
    "function safeTransferFrom(address from,address to,uint256 tokenId)",
    "function enterHunt(uint256)",
    "function resolveHunt(uint256)",
    "function fuseBeing(uint256,uint256)",
    "function observeExternal(address,uint256)",
    "function devourExternal(uint256,address,uint256)",
    "function devourTieredExternal(uint256,address,uint256,uint8,bytes32[])",
    "function observeCryptoPunk(address,uint256,uint8,bytes32[])",
    "function devourCryptoPunk(uint256,address,uint256)"
  ];
  const externalNftAbi=["function ownerOf(uint256) view returns (address)","function getApproved(uint256) view returns (address)","function isApprovedForAll(address,address) view returns (bool)","function supportsInterface(bytes4) view returns (bool)","function totalSupply() view returns (uint256)","function approve(address,uint256)","function setApprovalForAll(address,bool)"];
  const punkAbi=["function punkIndexToAddress(uint256) view returns (address)","function transferPunk(address,uint256)"];
  let provider, contract, indexProvider, indexContract, eventProvider, eventContract, walletContract, account, snapshot, ownedTokens=[], ownedSummaries=[], activeHunts=[], selectedBeingId=null, hasRevealedHistory=false;
  let curatedEntries=[], curatedTree=null, curatedRootValid=false, inspectedDevour=null;
  let walletNfts=[], walletNftsAccount="", walletNftsNext=null, walletNftsLoading=false;
  let beingIndexCache={};
  const beingArtCache=new Map();
  let eventCache=null;
  let isLoading=false, refreshQueued=false, isConnecting=false, transactionPending=false, stayDisconnected=false, injectedProvider=null;

  function setText(id, value) { const el=$(id); if(el) el.textContent=value; }
  function isAddress(value) { return typeof value==="string" && /^0x[a-fA-F0-9]{40}$/.test(value); }
  function hasMainContracts() { return [cfg.gameContract,cfg.beingNftContract||cfg.gameContract,cfg.oreContract,cfg.rendererContract].every(isAddress); }
  function short(a) { return a ? a.slice(0,6)+"…"+a.slice(-4) : "—"; }
  function fmt(n) { return Number(n).toLocaleString("en-US"); }
  function duration(blocks) {
    const seconds = Math.max(0, Number(blocks)) * Number(cfg.expectedBlockSeconds || 12);
    const d=Math.floor(seconds/86400), h=Math.floor((seconds%86400)/3600), m=Math.floor((seconds%3600)/60), s=seconds%60;
    return (d?d+"d ":"")+(h?h+"h ":"")+(m?m+"m ":"")+s+"s";
  }
  function secretKey(epoch) {
    return ["eb-secret", cfg.chainId, (cfg.gameContract||"pending").toLowerCase(), String(epoch), (account||"").toLowerCase()].join(":");
  }
  function readSecretRecord(epoch) {
    try {
      const raw=localStorage.getItem(secretKey(epoch));if(!raw)return null;
      if(raw.startsWith("0x"))return {version:0,chainId:cfg.chainId,gameContract:cfg.gameContract,wallet:account,epoch,secret:raw};
      const record=JSON.parse(raw);if(Number(record.chainId)!==Number(cfg.chainId)||String(record.gameContract).toLowerCase()!==String(cfg.gameContract).toLowerCase()||String(record.wallet).toLowerCase()!==String(account).toLowerCase()||Number(record.epoch)!==Number(epoch))return null;
      return record;
    }
    catch { status("Browser storage is unavailable. Enable site storage before Commit so the Reveal secret can be preserved.","error"); return null; }
  }
  function readSavedSecret(epoch){return readSecretRecord(epoch)?.secret||null;}
  function saveSecret(epoch, secret, commitment, derivation="legacy-random") {
    try { localStorage.setItem(secretKey(epoch),JSON.stringify({version:1,chainId:Number(cfg.chainId),gameContract:cfg.gameContract,wallet:account,epoch:Number(epoch),secret,commitment,derivation,createdAt:new Date().toISOString()})); return true; }
    catch { status("Could not save the Reveal secret. Enable site storage before committing.","error"); return false; }
  }
  function mintSecretMessage(epoch) {
    return ["Eternal Beings Mint Reveal","Chain ID: "+cfg.chainId,"Game: "+ethers.getAddress(cfg.gameContract),"Wallet: "+ethers.getAddress(account),"Epoch: "+Number(epoch),"","This signature reconstructs your Reveal secret. It sends no transaction and costs no gas."].join("\n");
  }
  async function deriveWalletSecret(epoch) {
    const signer=walletContract?.runner;
    if(!signer?.signMessage) throw new Error("Connected wallet cannot sign the Reveal reconstruction message.");
    status("Confirm the free off-chain signature in your wallet. This does not send a transaction…");
    const signature=await signer.signMessage(mintSecretMessage(epoch));
    return ethers.keccak256(signature);
  }
  function status(message, kind="") {
    const el=$("action-status"); if(!el) return;
    el.textContent=message; el.className="action-status "+kind;
  }
  function playStatus(message,kind="") { const el=$("play-status"); if(el){el.textContent=message;el.className="play-status "+kind;} }
  function esc(value){return String(value).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));}
  function walletProvider() { return window.okxwallet || window.ethereum || null; }
  function decodeDataUri(uri) {
    if(!uri||!uri.startsWith("data:")) return null;
    const comma=uri.indexOf(","), head=uri.slice(0,comma), data=uri.slice(comma+1);
    try { return head.includes(";base64") ? atob(data) : decodeURIComponent(data); } catch { return null; }
  }
  function beingArtState(tokenId,being){return [tokenId,being.stage,being.power,being.skill,being.mass,being.complexity,being.devours,being.fusions,being.scars,being.premiumDevours,being.markLuck].map(String).join(":");}
  function beingArtStorageKey(tokenId){return ["eb-art",cfg.chainId,String(cfg.gameContract).toLowerCase(),String(tokenId)].join(":");}
  function readBeingArt(tokenId,state){
    const key=beingArtStorageKey(tokenId),memory=beingArtCache.get(key);if(memory?.state===state)return memory;
    try{const saved=JSON.parse(localStorage.getItem(key)||"null");if(saved?.state===state&&saved.image){beingArtCache.set(key,saved);return saved;}}catch{}
    return null;
  }
  function saveBeingArt(tokenId,entry){
    const key=beingArtStorageKey(tokenId);beingArtCache.set(key,entry);
    try{localStorage.setItem(key,JSON.stringify(entry));}catch{}
  }
  function hashPair(a,b){const ordered=BigInt(a)<=BigInt(b)?[a,b]:[b,a];return ethers.keccak256(ethers.concat(ordered));}
  function buildCuratedTree(entries){
    const leaves=entries.map(entry=>ethers.solidityPackedKeccak256(["address","uint8"],[ethers.getAddress(entry.address.toLowerCase()),Number(entry.tier)]));
    const layers=[leaves];
    while(layers.at(-1).length>1){const current=layers.at(-1),next=[];for(let i=0;i<current.length;i+=2)next.push(i+1<current.length?hashPair(current[i],current[i+1]):current[i]);layers.push(next);}
    return {root:layers.at(-1)?.[0]||ethers.ZeroHash,layers};
  }
  function proofForCurated(index){const proof=[];let cursor=index;for(let level=0;level<curatedTree.layers.length-1;level++){const layer=curatedTree.layers[level],pair=cursor%2===0?cursor+1:cursor-1;if(pair<layer.length)proof.push(layer[pair]);cursor=Math.floor(cursor/2);}return proof;}
  async function loadCuratedRegistry(onchainRoot){
    if(!curatedEntries.length||!curatedTree){const response=await fetch("top-collections.ethereum.curated.json",{cache:"no-cache"});if(!response.ok)throw new Error("Top-100 registry unavailable");const entries=await response.json();const tree=buildCuratedTree(entries);curatedEntries=entries;curatedTree=tree;}
    curatedRootValid=curatedEntries.length===100&&curatedTree.root.toLowerCase()===String(onchainRoot).toLowerCase()&&(!cfg.topCollectionsRoot||curatedTree.root.toLowerCase()===cfg.topCollectionsRoot.toLowerCase());
    if(!curatedRootValid)throw new Error("Published Top-100 registry root does not match the deployed contract");
  }
  function summaryById(id){return ownedSummaries.find(item=>String(item.tokenId)===String(id));}
  function optionLabel(summary){return "Being #"+summary.tokenId+" — Stage "+summary.being.stage+" / PWR "+summary.being.power+" / SKL "+summary.being.skill;}
  function fillSelect(id,summaries,preferred){const select=$(id);if(!select)return;const previous=preferred??select.value;select.replaceChildren();if(!summaries.length){select.append(new Option("No eligible Being", ""));select.disabled=true;return;}summaries.forEach(item=>select.append(new Option(optionLabel(item),String(item.tokenId))));select.disabled=false;if(summaries.some(item=>String(item.tokenId)===String(previous)))select.value=String(previous);}
  function walletNftKey(nft){return String(nft.contract).toLowerCase()+":"+String(nft.tokenId);}
  function resetWalletNftPicker(message="Connect wallet to load NFTs."){
    walletNfts=[];walletNftsAccount="";walletNftsNext=null;walletNftsLoading=false;
    const select=$("devour-wallet-nft-select");if(select){select.replaceChildren(new Option("Connect wallet to load NFTs",""));select.disabled=true;}
    const refresh=$("devour-nft-refresh-btn");if(refresh)refresh.disabled=true;
    const more=$("devour-nft-more-btn");if(more){more.hidden=true;more.disabled=true;}
    const preview=$("devour-nft-preview"),image=$("devour-nft-image"),link=$("devour-nft-opensea");if(preview)preview.hidden=true;if(image){image.removeAttribute("src");image.hidden=true;}if(link){link.removeAttribute("href");link.hidden=true;}
    const contractInput=$("devour-contract"),tokenInput=$("devour-token-id");if(contractInput)contractInput.value="";if(tokenInput)tokenInput.value="";
    setText("devour-nft-list-status",message);
  }
  function renderWalletNftPicker(preferredValue){
    const select=$("devour-wallet-nft-select"),refresh=$("devour-nft-refresh-btn"),more=$("devour-nft-more-btn");if(!select)return;
    const previous=preferredValue??select.value;select.replaceChildren();
    if(!account){select.append(new Option("Connect wallet to load NFTs",""));select.disabled=true;}
    else if(walletNftsLoading&&!walletNfts.length){select.append(new Option("Loading wallet NFTs…",""));select.disabled=true;}
    else if(!walletNfts.length){select.append(new Option("No selectable ERC-721 NFTs found",""));select.disabled=true;}
    else{
      select.append(new Option("Select an NFT ("+walletNfts.length+" loaded)",""));
      walletNfts.forEach(nft=>select.append(new Option((nft.name||nft.collection||short(nft.contract))+" — #"+nft.tokenId,walletNftKey(nft))));
      select.disabled=false;if(walletNfts.some(nft=>walletNftKey(nft)===previous))select.value=previous;
    }
    if(refresh)refresh.disabled=!account||walletNftsLoading;
    if(more){more.hidden=!walletNftsNext;more.disabled=walletNftsLoading||!walletNftsNext;}
  }
  function renderSelectedWalletNft(nft){
    const preview=$("devour-nft-preview"),image=$("devour-nft-image"),link=$("devour-nft-opensea");if(!preview)return;
    preview.hidden=!nft;if(!nft){if(link){link.removeAttribute("href");link.hidden=true;}return;}
    if(image){if(nft.image){image.src=nft.image;image.hidden=false;}else{image.removeAttribute("src");image.hidden=true;}}
    setText("devour-nft-name",nft.name||("NFT #"+nft.tokenId));setText("devour-nft-collection",nft.collection||"Unlabelled collection");setText("devour-nft-identity",short(nft.contract)+" / #"+nft.tokenId);
    if(link){link.href="https://opensea.io/assets/ethereum/"+encodeURIComponent(nft.contract)+"/"+encodeURIComponent(nft.tokenId);link.hidden=false;}
  }
  async function selectWalletNft(){
    const selected=$("devour-wallet-nft-select")?.value,nft=walletNfts.find(item=>walletNftKey(item)===selected);if(!nft){renderSelectedWalletNft(null);return;}
    $("devour-contract").value=nft.contract;$("devour-token-id").value=nft.tokenId;renderSelectedWalletNft(nft);
    inspectedDevour=null;setDevourButtons(null);setDevourReadouts("Checking the selected wallet NFT against current on-chain state…","Ownership, contract type, eligibility, proof, approval, hold, and cooldown are being verified.");
    await inspectDevour();
  }
  async function loadWalletNfts({force=false,append=false}={}){
    if(!account||!walletContract||!ownedSummaries.length){resetWalletNftPicker(account?"Claim or receive a Being before selecting an NFT to Devour.":"Connect wallet to load NFTs.");return;}
    const owner=account.toLowerCase();if(walletNftsLoading)return;if(!force&&!append&&walletNftsAccount===owner)return;if(append&&!walletNftsNext)return;
    if(force||walletNftsAccount!==owner){walletNfts=[];walletNftsNext=null;walletNftsAccount=owner;renderSelectedWalletNft(null);const picker=$("devour-wallet-nft-select"),contractInput=$("devour-contract"),tokenInput=$("devour-token-id");if(picker)picker.value="";if(contractInput)contractInput.value="";if(tokenInput)tokenInput.value="";inspectedDevour=null;setDevourButtons(null);setDevourReadouts("Select a Being and one NFT owned by the connected wallet.","The contract route will appear after the NFT is checked.");}
    walletNftsLoading=true;renderWalletNftPicker();setText("devour-nft-list-status",append?"Loading more owned NFTs…":"Loading owned NFTs from the Ethereum index…");
    try{
      const endpoint=cfg.walletNftEndpoint||"/api/wallet-nfts",params=new URLSearchParams({owner:account});if(append&&walletNftsNext)params.set("cursor",walletNftsNext);
      const response=await fetch(endpoint+"?"+params.toString(),{headers:{Accept:"application/json"},cache:"no-store"}),data=await response.json().catch(()=>({}));
      if(!response.ok)throw new Error(data.error||("NFT index returned HTTP "+response.status));
      const incoming=Array.isArray(data.tokens)?data.tokens.filter(nft=>isAddress(nft.contract)&&/^\d+$/.test(String(nft.tokenId))&&nft.contract.toLowerCase()!==cfg.gameContract.toLowerCase()):[];
      const merged=new Map(walletNfts.map(nft=>[walletNftKey(nft),nft]));incoming.forEach(nft=>merged.set(walletNftKey(nft),nft));walletNfts=[...merged.values()];walletNftsNext=typeof data.next==="string"&&data.next?data.next:null;
      setText("devour-nft-list-status",walletNfts.length?walletNfts.length+" owned ERC-721 NFT(s) loaded. Select one; ownership is checked again on-chain.":"No external ERC-721 NFTs were returned. Use manual entry only if an owned NFT is missing from the index.");
    }catch(err){setText("devour-nft-list-status","Automatic NFT list unavailable: "+(err.message||err)+". Manual entry remains available.");const manual=document.querySelector(".devour-manual");if(manual)manual.open=true;}
    finally{walletNftsLoading=false;renderWalletNftPicker();}
  }
  function resetPlayConsole(){
    ["hunt-being-select","fusion-parent-select","fusion-sacrifice-select","devour-being-select"].forEach(id=>fillSelect(id,[]));
    ["hunt-enter-btn","fusion-btn","devour-next-btn"].forEach(id=>{const el=$(id);if(el)el.disabled=true;});
    ["fusion-confirm","devour-confirm"].forEach(id=>{const el=$(id);if(el){el.checked=false;el.disabled=true;}});
    resetWalletNftPicker();setDevourButtons(null);setText("hunt-readout","Connect the owner wallet to inspect Hunt readiness.");setText("fusion-preview","Two owned Beings are required.");setText("devour-readout","Select a Being and one NFT owned by the connected wallet.");setText("devour-technical-readout","The contract route will appear after the NFT is checked.");
    const list=$("active-hunts-list");if(list)list.innerHTML='<div class="play-empty">No wallet connected.</div>';
    playStatus("Connect the owner wallet to load gameplay actions.");
  }
  function setGuide(stage) {
    const order=["connect","commit","reveal","claim","being","play"];
    const index=order.indexOf(stage);
    document.querySelectorAll(".guide-node").forEach(node=>{
      const i=order.indexOf(node.dataset.stage);
      node.classList.toggle("done",i>=0&&i<index);
      node.classList.toggle("active",node.dataset.stage===stage||(stage==="play"&&node.dataset.branch));
    });
    setText("guide-current",stage==="play"?"YOUR BEING IS ACTIVE — CHOOSE A GAMEPLAY BRANCH":stage.toUpperCase()+" IS YOUR NEXT CORE STEP");
  }
  function setExplorerLinks() {
    const entries=[["game-link",cfg.gameContract],["nft-link",cfg.beingNftContract],["ore-link",cfg.oreContract],["renderer-link",cfg.rendererContract]];
    entries.forEach(([id,address])=>{const el=$(id);if(!el)return;if(!isAddress(address)){el.textContent="PENDING";el.removeAttribute("href");return;}el.textContent=short(address);el.href=cfg.explorerUrl+"/address/"+address;});
    setText("network-name",cfg.networkName); setText("chain-id",String(cfg.chainId));
  }
  function resetPlayerUi(message="Wallet disconnected locally.") {
    account=null; walletContract=null; ownedTokens=[]; ownedSummaries=[]; activeHunts=[]; selectedBeingId=null; inspectedDevour=null;eventCache=null;beingIndexCache={};hasRevealedHistory=false;
    setText("wallet-label","CONNECT WALLET");
    setText("player-wallet","NOT CONNECTED");
    setText("player-state","CONNECT WALLET TO CHECK");
    setText("claim-availability","—");
    ["commit-btn","reveal-btn","claim-btn"].forEach(id=>{const b=$(id); if(b){b.disabled=true;b.classList.remove("ready");}});
    $("being-empty").hidden=false; $("being-card").hidden=true;
    setText("being-empty-title","CONNECT YOUR WALLET");
    setText("being-empty-copy","Your claimed NFTs, evolution attributes, Hunt state, cooldown, and next gameplay action will appear here.");
    const list=$("being-list"); if(list) list.innerHTML='<div class="lottery-empty">Connect your wallet to list owned Beings.</div>';
    const lottery=$("lottery-list"); if(lottery) lottery.innerHTML='<div class="lottery-empty">Connect your wallet to inspect Commit, Reveal, draw, and Claim results.</div>';
    const transferInput=$("transfer-to"); if(transferInput) transferInput.value="";
    setText("transfer-status","Select a Being, enter a recipient, then confirm in the owner wallet.");
    resetPlayConsole();
    const connectBtn=$("connect-btn"), disconnectBtn=$("disconnect-btn");
    if(connectBtn) connectBtn.hidden=false;
    if(disconnectBtn) disconnectBtn.hidden=true;
    setGuide("connect");
    status(message,"ok");
  }
  async function ensureEventCache(){
    const lower=(account||"").toLowerCase();
    if(eventCache?.account===lower&&eventCache.startBlock!==undefined&&Object.values(eventCache.streams).every(stream=>stream.through<=snapshot.block))return eventCache;
    const startBlock=Number(cfg.deploymentBlock||await contract.startBlock());
    eventCache={account:lower,startBlock,streams:{transfers:{through:startBlock-1,values:new Set()},hunts:{through:startBlock-1,values:new Set()},commits:{through:startBlock-1,values:new Set()}}};
    return eventCache;
  }
  function wait(ms){return new Promise(resolve=>setTimeout(resolve,ms));}
  async function queryAccountEvents(filter,from,to){
    let lastError;
    for(let attempt=0;attempt<5;attempt++){
      try{return await (eventContract||contract).queryFilter(filter,from,to);}
      catch(err){lastError=err;if(attempt<4)await wait(400*(2**attempt));}
    }
    const rpcMessage=lastError?.error?.message||lastError?.info?.error?.message||lastError?.shortMessage||lastError?.message||"event query failed";
    throw new Error("RPC event service unavailable for blocks "+from+"-"+to+": "+rpcMessage);
  }
  async function scanAccountEvents(name,filter,collect){
    const cache=await ensureEventCache(),stream=cache.streams[name];
    for(let from=stream.through+1;from<=snapshot.block;from+=eventLogChunkBlocks){const to=Math.min(snapshot.block,from+eventLogChunkBlocks-1),events=await queryAccountEvents(filter,from,to);events.forEach(event=>collect(stream.values,event));stream.through=to;}
    return stream.values;
  }
  async function participatingEpochs(){
    const hint=Number(cfg.playerMintEpochHints?.[(account||"").toLowerCase()]);
    if(Number.isInteger(hint)&&hint>=0)return [hint];
    if(!hasRevealedHistory)return [];
    if(indexContract&&Number(snapshot?.e||0)<=500){
      const candidates=Array.from({length:Number(snapshot.e)+1},(_,epoch)=>epoch);
      const commitments=await Promise.all(candidates.map(epoch=>indexContract.commitments(epoch,account)));
      return candidates.filter((_,index)=>commitments[index]!==zero).sort((a,b)=>b-a);
    }
    const epochs=await scanAccountEvents("commits",contract.filters.Committed(null,account),(values,event)=>values.add(Number(event.args.epoch)));
    const result=new Set(epochs);
    return [...result].sort((a,b)=>b-a);
  }
  function configuredTokenHints(){
    const hinted=cfg.playerTokenHints?.[(account||"").toLowerCase()];
    return (Array.isArray(hinted)?hinted:hinted===undefined?[]:[hinted]).map(String);
  }
  async function loadBeingIndex(mode="owners"){
    const lower=(account||"").toLowerCase();
    const cached=beingIndexCache[mode];
    if(cached?.account===lower&&Date.now()-cached.loadedAt<30000)return cached;
    const endpoint=cfg.beingIndexEndpoint||"/api/being-index";
    const response=await fetch(endpoint+"?"+new URLSearchParams({owner:account,mode}),{headers:{Accept:"application/json"},cache:"no-store"});
    const data=await response.json().catch(()=>({}));
    if(!response.ok)throw new Error(data.error||("Being index returned HTTP "+response.status));
    const tokenIds=(Array.isArray(data.tokenIds)?data.tokenIds:[]).map(String).filter(id=>/^\d+$/.test(id)&&BigInt(id)>0n&&BigInt(id)<=BigInt(data.total||0));
    return beingIndexCache[mode]={account:lower,loadedAt:Date.now(),block:Number(data.block||0),total:Number(data.total||0),tokenIds};
  }
  async function findOwnedTokens(knownBalance) {
    const lower=(account||"").toLowerCase();
    const balance=knownBalance===undefined?Number(await contract.balanceOf(account)):Number(knownBalance);
    const hints=configuredTokenHints(),hintedOwned=[];
    for(const id of hints){try{if((await contract.ownerOf(id)).toLowerCase()===lower)hintedOwned.push(BigInt(id));}catch{}}
    if(hintedOwned.length===balance)return hintedOwned.sort((a,b)=>Number(a-b));
    try{
      const indexed=await loadBeingIndex(),verified=[];
      for(const id of indexed.tokenIds){try{if((await contract.ownerOf(id)).toLowerCase()===lower)verified.push(BigInt(id));}catch{}}
      if(verified.length===balance)return verified.sort((a,b)=>Number(a-b));
    }catch{}
    if(balance>0&&indexContract&&Number(snapshot?.total||0)<=2000){
      const candidates=Array.from({length:Number(snapshot.total)},(_,index)=>BigInt(index+1));
      const owners=await Promise.all(candidates.map(async id=>{try{return (await indexContract.ownerOf(id)).toLowerCase()===lower?id:null;}catch{return null;}}));
      const direct=owners.filter(id=>id!==null);
      if(direct.length===balance)return direct.sort((a,b)=>Number(a-b));
    }
    const ids=new Set(await scanAccountEvents("transfers",contract.filters.Transfer(null,account,null),(values,event)=>values.add(String(event.args.tokenId))));
    hints.forEach(id=>ids.add(id));
    const checked=[];
    for(const id of ids){
      try {
        const owner=await contract.ownerOf(id);
        if(owner.toLowerCase()===lower) checked.push(BigInt(id));
      } catch {}
    }
    if(checked.length!==balance)throw new Error("Owned Being event index is incomplete: chain balance "+balance+", verified tokens "+checked.length+". Refresh or change RPC before acting.");
    return checked.sort((a,b)=>Number(a-b));
  }
  async function findActiveHunts(){
    const hints=configuredTokenHints();
    let entries=[],indexedLoaded=false;
    if(hints.length){entries=await Promise.all(hints.map(async id=>({id,hunt:await contract.hunts(id)})));}
    else try{
      const indexed=await loadBeingIndex("hunts");indexedLoaded=true;
      entries=await Promise.all(indexed.tokenIds.map(async id=>({id,hunt:await contract.hunts(id)})));
    }catch{}
    if(!hints.length&&!indexedLoaded&&indexContract&&Number(snapshot?.total||0)<=2000){
      const ids=Array.from({length:Number(snapshot.total)},(_,index)=>String(index+1));
      const hunts=await Promise.all(ids.map(id=>indexContract.hunts(id)));
      entries=ids.map((id,index)=>({id,hunt:hunts[index]})).filter(entry=>entry.hunt.owner.toLowerCase()===(account||"").toLowerCase());
    }else if(!hints.length&&!indexedLoaded&&!indexContract){
      const ids=await scanAccountEvents("hunts",contract.filters.HuntEntered(null,account),(values,event)=>values.add(String(event.args.tokenId)));
      entries=await Promise.all([...ids].map(async id=>({id,hunt:await contract.hunts(id)})));
    }
    const rows=[];
    for(const {id,hunt} of entries){if(hunt.owner.toLowerCase()===(account||"").toLowerCase()){const [being,cooldown]=await Promise.all([contract.getBeing(id),contract.cooldownUntil(id)]);rows.push({tokenId:BigInt(id),being,owner:cfg.gameContract,hunt,cooldown,activeHunt:true});}}
    return rows.sort((a,b)=>Number(a.tokenId-b.tokenId));
  }
  function renderActiveHunts(){
    const list=$("active-hunts-list");if(!list)return;list.replaceChildren();
    if(!activeHunts.length){list.innerHTML='<div class="play-empty">No active Hunt. Choose an available Being above.</div>';return;}
    activeHunts.forEach(summary=>{
      const elapsed=Math.max(0,snapshot.block-Number(summary.hunt.startBlock)),canResolve=elapsed>0;
      const row=document.createElement("div");row.className="hunt-row";
      row.innerHTML='<div><b>BEING #'+summary.tokenId+' // SCENE '+summary.hunt.sceneId+'</b><span>'+fmt(elapsed)+' blocks elapsed // '+fmt(summary.hunt.endurance)+' max active blocks // difficulty '+fmt(summary.hunt.difficulty)+'</span></div>';
      const button=document.createElement("button");button.type="button";button.textContent=canResolve?"RESOLVE + RETURN":"WAIT 1 BLOCK";button.disabled=!canResolve;button.addEventListener("click",()=>resolveHunt(summary.tokenId));row.appendChild(button);list.appendChild(row);
    });
  }
  function updateHuntReadout(){
    const summary=summaryById($("hunt-being-select")?.value);const button=$("hunt-enter-btn");
    if(!summary){setText("hunt-readout","No available Being selected.");if(button)button.disabled=true;return;}
    const remaining=Math.max(0,Number(summary.cooldown)-snapshot.block),ready=remaining===0;
    setText("hunt-readout","Being #"+summary.tokenId+" // "+(ready?"READY":"COOLDOWN: "+fmt(remaining)+" blocks / about "+duration(remaining))+"\nThe NFT moves into Game custody until you resolve the Hunt.");
    if(button)button.disabled=!ready||!walletContract;
  }
  function updateFusionPreview(){
    const parent=summaryById($("fusion-parent-select")?.value),sacrifice=summaryById($("fusion-sacrifice-select")?.value),confirm=$("fusion-confirm"),button=$("fusion-btn");
    const valid=parent&&sacrifice&&String(parent.tokenId)!==String(sacrifice.tokenId);
    if(confirm)confirm.disabled=!valid;
    if(!valid){setText("fusion-preview",ownedSummaries.length<2?"Two owned Beings are required.":"Parent and sacrifice must be different Beings.");if(button)button.disabled=true;return;}
    const p=parent.being,s=sacrifice.being,parentReady=Number(parent.cooldown)<=snapshot.block,sacrificeReady=Number(sacrifice.cooldown)<=snapshot.block;
    const mass=BigInt(p.mass)+BigInt(s.mass),complexity=BigInt(p.complexity)+(BigInt(s.complexity)*70n)/100n+1n,power=BigInt(p.power)+1n+BigInt(s.power)/2n,skill=BigInt(p.skill)+1n+BigInt(s.skill)/2n;
    setText("fusion-preview","SURVIVES: Being #"+parent.tokenId+" // BURNED: Being #"+sacrifice.tokenId+"\nBase result before possible mutation: Mass "+mass+", Complexity "+complexity+", Power "+power+", Skill "+skill+".\nCooldowns: parent "+(parentReady?"READY":"BLOCK "+parent.cooldown)+" // sacrifice "+(sacrificeReady?"READY":"BLOCK "+sacrifice.cooldown));
    if(button)button.disabled=!(valid&&parentReady&&sacrificeReady&&confirm?.checked&&walletContract);
  }
  function renderPlayConsole(){
    fillSelect("hunt-being-select",ownedSummaries,selectedBeingId);fillSelect("fusion-parent-select",ownedSummaries,selectedBeingId);fillSelect("fusion-sacrifice-select",ownedSummaries,ownedSummaries[1]?.tokenId);fillSelect("devour-being-select",ownedSummaries,selectedBeingId);
    const fusionConfirm=$("fusion-confirm"),devourConfirm=$("devour-confirm");if(fusionConfirm){fusionConfirm.checked=false;fusionConfirm.disabled=ownedSummaries.length<2;}if(devourConfirm){devourConfirm.checked=false;devourConfirm.disabled=true;}
    inspectedDevour=null;setDevourButtons(null);renderActiveHunts();updateHuntReadout();updateFusionPreview();renderWalletNftPicker();void loadWalletNfts();
    playStatus(ownedSummaries.length||activeHunts.length?ownedSummaries.length+" owned Being(s) // "+activeHunts.length+" active Hunt(s) loaded at block "+fmt(snapshot.block):"No owned or hunting Being was found for this wallet.",ownedSummaries.length||activeHunts.length?"ok":"warn");
  }
  async function findOwnedToken() {
    const tokens=await findOwnedTokens();
    if(tokens.length) return tokens[tokens.length-1];
    throw new Error("Owned Being token not found. Add this wallet and tokenId to playerTokenHints after deployment.");
  }
  async function readBeingSummary(tokenId) {
    const [being,owner,hunt,cooldown]=await Promise.all([contract.getBeing(tokenId),contract.ownerOf(tokenId),contract.hunts(tokenId),contract.cooldownUntil(tokenId)]);
    const activeHunt=hunt.owner!==ethers.ZeroAddress;
    return {tokenId,being,owner,hunt,cooldown,activeHunt};
  }
  async function renderBeingDetail(summary) {
    const {tokenId,being,owner,cooldown,activeHunt}=summary;
    selectedBeingId=tokenId;
    $("being-empty").hidden=true; $("being-card").hidden=false;
    setText("being-id","BEING #"+tokenId); setText("being-owner",short(owner));
    setText("being-stage",String(being.stage)); setText("being-power",fmt(being.power)); setText("being-skill",fmt(being.skill));
    setText("being-mass",fmt(being.mass)); setText("being-complexity",fmt(being.complexity));
    setText("being-devours",fmt(being.devours)); setText("being-fusions",fmt(being.fusions)); setText("being-scars",fmt(being.scars));
    setText("being-premium-devours",being.premiumDevours===undefined?"—":fmt(being.premiumDevours));
    setText("being-mark-luck",being.markLuck===undefined?"—":fmt(being.markLuck));
    setText("being-cooldown",Number(cooldown)>snapshot.block?duration(Number(cooldown)-snapshot.block):"READY");
    setText("being-action",activeHunt?"HUNT ACTIVE — RESOLVE WHEN READY":"READY FOR HUNT / DEVOUR / FUSION");
    setGuide("play");
    const image=$("being-image"),artState=beingArtState(tokenId,being),cachedArt=readBeingArt(tokenId,artState);
    image.dataset.tokenId=String(tokenId);
    if(cachedArt){image.src=cachedArt.image;image.alt=cachedArt.name||("Eternal Being #"+tokenId);image.hidden=false;}
    else {image.removeAttribute("src");image.alt="Loading Eternal Being #"+tokenId;image.hidden=true;}
    if(cachedArt){
      $("being-explorer").href=cfg.explorerUrl+"/token/"+cfg.beingNftContract+"?a="+tokenId;
      const transferStatus=$("transfer-status");if(transferStatus)transferStatus.textContent="Selected Being #"+tokenId+". Transfers require the owner wallet.";
      return;
    }
    void contract.tokenURI(tokenId).then(uri=>{
      if(String(selectedBeingId)!==String(tokenId))return;
      const jsonText=decodeDataUri(uri),metadata=jsonText?JSON.parse(jsonText):null;
      if(metadata?.image){const art={state:artState,image:metadata.image,name:metadata.name||("Eternal Being #"+tokenId)};saveBeingArt(tokenId,art);image.src=art.image;image.alt=art.name;image.hidden=false;}
      else image.alt="Eternal Being metadata image unavailable";
    }).catch(()=>{image.alt="Eternal Being metadata image unavailable";});
    $("being-explorer").href=cfg.explorerUrl+"/token/"+cfg.beingNftContract+"?a="+tokenId;
    const transferStatus=$("transfer-status");
    if(transferStatus) transferStatus.textContent="Selected Being #"+tokenId+". Transfers require the owner wallet.";
  }
  function renderBeingList(summaries, selectedId) {
    const list=$("being-list"); if(!list) return;
    list.replaceChildren();
    summaries.forEach(summary=>{
      const b=summary.being, tokenId=String(summary.tokenId);
      const item=document.createElement("button");
      item.type="button";
      item.className="being-list-item"+(String(selectedId)===tokenId?" active":"");
      item.dataset.tokenId=tokenId;
      item.innerHTML='<span>BEING #'+tokenId+'</span><b>STAGE '+b.stage+' // PWR '+b.power+' // SKL '+b.skill+'</b><em>'+(summary.activeHunt?'HUNTING':Number(summary.cooldown)>snapshot.block?'COOLDOWN':'READY')+'</em>';
      item.addEventListener("click",async()=>{
        document.querySelectorAll(".being-list-item").forEach(node=>node.classList.remove("active"));
        item.classList.add("active");
        await renderBeingDetail(summary);
      });
      list.appendChild(item);
    });
  }
  async function load() {
    if(isLoading){refreshQueued=true;return;}
    isLoading=true;
    try {
      if(!window.ethers) throw new Error("ethers library unavailable");
      if(!hasMainContracts()) {
        setExplorerLinks();
        snapshot=null;
        setText("live-dot","● CONFIG");
        setText("current-block","MAINNET ADDRESSES NOT SET");
        setText("current-epoch","—");
        setText("phase-name","PENDING DEPLOYMENT");
        setText("phase-detail","Mainnet timing is configured: 7,200 blocks per epoch, 4,800 commit blocks, 333 slots per epoch.");
        setText("next-state","DEPLOY MAINNET CONTRACTS");
        setText("time-left","—");
        setText("blocks-left","Enter deployed contract addresses in instance-config.js.");
        setText("epoch-progress","After deployment, fill instance-config.js and refresh.");
        status("Mainnet profile is active, but contract addresses are not set yet.","warn");
        return;
      }
      if(!provider||!contract){provider=new ethers.JsonRpcProvider(cfg.rpcUrl,cfg.chainId,{staticNetwork:true});contract=new ethers.Contract(cfg.gameContract,abi,provider);}
      if(!indexProvider||!indexContract){indexProvider=new ethers.JsonRpcProvider(cfg.rpcUrl,cfg.chainId,{staticNetwork:true,batchMaxCount:100,batchStallTime:10});indexContract=new ethers.Contract(cfg.gameContract,abi,indexProvider);}
      if(!eventProvider||!eventContract){eventProvider=cfg.eventRpcUrl&&cfg.eventRpcUrl!==cfg.rpcUrl?new ethers.JsonRpcProvider(cfg.eventRpcUrl,cfg.chainId,{staticNetwork:true}):provider;eventContract=eventProvider===provider?contract:new ethers.Contract(cfg.gameContract,abi,eventProvider);}
      const [block,epoch,epochBlocks,commitBlocks,perEpoch,max,total,alive,cooldownBlocks,maxEndurance,unknownHold,punkObservation,topRoot,onchainOre,onchainRenderer,rpcChain,gameCode] = await Promise.all([
        provider.getBlockNumber(), contract.currentEpoch(), contract.EPOCH_BLOCKS(), contract.COMMIT_BLOCKS(),
        contract.MINTS_PER_EPOCH(), contract.MAX_BEINGS(), contract.totalMinted(), contract.aliveSupply(),contract.COOLDOWN_BLOCKS(),contract.MAX_ENDURANCE(),contract.UNKNOWN_HOLD_BLOCKS(),contract.CRYPTOPUNK_OBSERVATION_BLOCKS(),contract.topCollectionsRoot(),contract.ore(),contract.renderer(),provider.send("eth_chainId",[]),provider.getCode(cfg.gameContract)
      ]);
      if(Number(rpcChain)!==Number(cfg.chainId))throw new Error("RPC chain mismatch: expected "+cfg.chainId+", received "+Number(rpcChain));
      if(gameCode==="0x")throw new Error("Configured Game address has no deployed bytecode");
      if(onchainOre.toLowerCase()!==cfg.oreContract.toLowerCase())throw new Error("ORE contract mismatch");
      if(onchainRenderer.toLowerCase()!==cfg.rendererContract.toLowerCase())throw new Error("Renderer contract mismatch");
      if(cfg.topCollectionsRoot&&String(topRoot).toLowerCase()!==cfg.topCollectionsRoot.toLowerCase())throw new Error("Top-100 root mismatch");
      if(Number(epochBlocks)!==Number(cfg.epochBlocks)||Number(commitBlocks)!==Number(cfg.commitBlocks)||Number(perEpoch)!==Number(cfg.mintsPerEpoch))throw new Error("Configured Mint constants do not match the deployed Game");
      const e=Number(epoch), eb=Number(epochBlocks), cb=Number(commitBlocks);
      const start=Number(await contract.epochStart(e));
      const offset=block-start;
      const phase=offset<cb?"COMMIT":"REVEAL";
      const nextBlock=phase==="COMMIT"?start+cb:start+eb;
      const previous=e>0?e-1:null;
      const [released,reveals,limit,claimed] = await Promise.all([
        contract.releasedMintCap(e), contract.revealedCount(e), contract.epochClaimLimit(e), contract.epochClaimedCount(e)
      ]);
      snapshot={block,e,eb,cb,start,offset,phase,nextBlock,previous,perEpoch:Number(perEpoch),max:Number(max),total:Number(total),cooldownBlocks:Number(cooldownBlocks),maxEndurance:Number(maxEndurance),unknownHold:Number(unknownHold),punkObservation:Number(punkObservation),topRoot};
      try{await loadCuratedRegistry(topRoot);}catch(err){curatedRootValid=false;playStatus(err.message,"error");}
      const totalRounds=Math.ceil(Number(max)/Number(perEpoch));
      setText("live-dot","● LIVE"); setText("current-block",fmt(block)); setText("current-epoch","#"+e+" // ROUND "+(e+1)+" // "+totalRounds+" MINIMUM");
      setText("phase-name",phase); setText("phase-detail",phase==="COMMIT"?"Commitments are open now":"Reveal your saved secret now");
      setText("next-state",phase==="COMMIT"?"REVEAL OPENS":"EPOCH CLOSES / CLAIM OPENS");
      setText("blocks-left",fmt(nextBlock-block)+" blocks"); setText("time-left",duration(nextBlock-block));
      setText("epoch-progress",fmt(offset)+" / "+fmt(eb)+" blocks");
      setText("total-minted",fmt(total)+" / "+fmt(max)); setText("alive-supply",fmt(alive));
      setText("released-cap",fmt(released)); setText("epoch-reveals",fmt(reveals));
      setText("epoch-claim-limit",Number(limit)===0?"SET AT FIRST CLAIM":fmt(limit));
      setText("epoch-claimed",fmt(claimed)); setText("daily-release",fmt(perEpoch));
      setText("previous-epoch",previous===null?"NONE":"#"+previous);
      setText("claim-availability",previous===null?"No completed epoch yet":"Epoch #"+previous+" is open for eligible claims");
      const pct=Math.min(100,Math.max(0,offset/eb*100)); $("epoch-bar").style.width=pct+"%";
      await updateActions();
      if(account&&!transactionPending) await loadPlayerDashboard();
      if(!transactionPending) status("Chain state refreshed at block "+fmt(block),"ok");
    } catch (err) {
      setText("live-dot","● OFFLINE"); status("Unable to read the configured instance: "+(err.shortMessage||err.message),"error");
    } finally {
      isLoading=false;
      if(refreshQueued){refreshQueued=false;queueMicrotask(load);}
    }
  }
  async function connect() {
    if(isConnecting) return;
    stayDisconnected=false;
    isConnecting=true;
    try {
      const injected=walletProvider();
      if(!injected) throw new Error(location.protocol==="file:"?"Wallets cannot connect to this local file. Open the page through HTTPS or localhost.":"No compatible EVM browser wallet detected");
      if(!hasMainContracts()) throw new Error("Mainnet contract addresses are not set yet. Fill instance-config.js after deployment, then connect.");
      let browserProvider=new ethers.BrowserProvider(injected);
      await browserProvider.send("eth_requestAccounts",[]);
      const network=await browserProvider.getNetwork();
      if(Number(network.chainId)!==Number(cfg.chainId)) {
        await injected.request({method:"wallet_switchEthereumChain",params:[{chainId:"0x"+Number(cfg.chainId).toString(16)}]});
        browserProvider=new ethers.BrowserProvider(injected);
      }
      const verifiedNetwork=await browserProvider.getNetwork();if(Number(verifiedNetwork.chainId)!==Number(cfg.chainId))throw new Error("Wallet network switch did not complete. Expected chain "+cfg.chainId+".");
      const walletCode=await browserProvider.getCode(cfg.gameContract);if(walletCode==="0x")throw new Error("Game contract bytecode is unavailable through the connected wallet network.");
      const signer=await browserProvider.getSigner(); account=await signer.getAddress();
      injectedProvider=injected;
      provider=browserProvider;
      contract=new ethers.Contract(cfg.gameContract,abi,browserProvider);
      walletContract=new ethers.Contract(cfg.gameContract,abi,signer);
      setText("wallet-label",short(account)); status("Wallet connected. Reading player state…");
      const connectBtn=$("connect-btn"), disconnectBtn=$("disconnect-btn");
      if(connectBtn) connectBtn.hidden=false;
      if(disconnectBtn) disconnectBtn.hidden=false;
      await updateActions();
      await loadPlayerDashboard();
    } catch(err){status(err.shortMessage||err.message,"error");}
    finally { isConnecting=false; }
  }
  async function updateClaimAction(previousEpoch,expectedAccount){
    try{
      const candidates=(await participatingEpochs()).filter(epoch=>epoch<=previousEpoch);
      if((account||"").toLowerCase()!==expectedAccount.toLowerCase())return;
      let claimableEpoch=null;
      for(const candidate of candidates){
        const [didReveal,didClaim]=await Promise.all([contract.revealed(candidate,expectedAccount),contract.claimedEpoch(candidate,expectedAccount)]);
        if(!didReveal||didClaim)continue;
        try{await walletContract.claimMint.staticCall(candidate);claimableEpoch=candidate;break;}catch(err){if(err?.code!=="CALL_EXCEPTION")throw err;}
      }
      if((account||"").toLowerCase()!==expectedAccount.toLowerCase())return;
      const claimBtn=$("claim-btn");
      if(claimableEpoch!==null){claimBtn.disabled=false;claimBtn.classList.add("ready");claimBtn.dataset.epoch=String(claimableEpoch);setText("claim-availability","Epoch #"+claimableEpoch+" is eligible and ready to claim");}
      else if(candidates.length)setText("claim-availability","No completed epoch is currently claimable by this wallet");
    }catch(err){if((account||"").toLowerCase()===expectedAccount.toLowerCase())setText("claim-availability","Historical Claim check is temporarily unavailable");}
  }
  async function updateActions() {
    if(!snapshot) return;
    const commitBtn=$("commit-btn"), revealBtn=$("reveal-btn"), claimBtn=$("claim-btn");
    [commitBtn,revealBtn,claimBtn].forEach(b=>{b.disabled=true;b.classList.remove("ready")});
    if(!account||!walletContract){setText("player-state","CONNECT WALLET TO CHECK");setGuide("connect");return;}
    try {
      const e=snapshot.e, prev=snapshot.previous;
      const [minted,commitment,revealedNow,revealedAny] = await Promise.all([
        contract.hasMintedBeing(account), contract.commitments(e,account), contract.revealed(e,account),contract.hasRevealedAny(account)
      ]);
      hasRevealedHistory=Boolean(revealedAny);
      let player=minted?"MINT COMPLETE":snapshot.phase==="COMMIT"?(commitment===zero?"READY TO COMMIT":"COMMITTED"):(revealedNow?"REVEALED":commitment===zero?"NO COMMITMENT":"READY TO REVEAL");
      setText("player-state",player);
      setGuide(minted?"being":snapshot.phase==="COMMIT"?(commitment===zero?"commit":"reveal"):(revealedNow?"claim":"reveal"));
      if(!minted && snapshot.phase==="COMMIT" && commitment===zero){commitBtn.disabled=false;commitBtn.classList.add("ready");}
      const savedSecret=readSavedSecret(e);
      const savedCommitment=savedSecret?ethers.solidityPackedKeccak256(["address","uint256","bytes32"],[account,e,savedSecret]):null,secretMatches=savedCommitment===commitment;
      if(!minted && snapshot.phase==="REVEAL" && commitment!==zero && !revealedNow){revealBtn.disabled=false;revealBtn.classList.add("ready");status(secretMatches?"Reveal is open. Your wallet is ready for the next transaction.":"Reveal is open. Click Reveal to reconstruct and verify the secret with your wallet.","ok");}
      if(!minted&&prev!==null&&hasRevealedHistory)void updateClaimAction(prev,account);
    } catch(err){status("Could not read wallet state: "+(err.shortMessage||err.message),"error");}
  }
  async function loadSecondaryPlayerState() {
    let lotteryError=null;
    try { activeHunts=await findActiveHunts(); } catch(err) { playStatus("Hunt state is temporarily unavailable: "+(err.shortMessage||err.message),"error"); }
    try { await loadLotteryHistory(); } catch(err) { lotteryError=err; }
    if(lotteryError) {
      const lottery=$("lottery-list");
      if(lottery) lottery.innerHTML='<div class="lottery-empty">Mint history is temporarily unavailable: '+String(lotteryError.shortMessage||lotteryError.message||"read failed").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]))+'</div>';
    }
    renderPlayConsole();
  }
  async function loadPlayerDashboard() {
    if(!account||!contract||!snapshot) return;
    try {
      setText("player-wallet",short(account));
      const balance=Number(await contract.balanceOf(account));
      if(balance===0){
        activeHunts=[];
        ownedTokens=[]; ownedSummaries=[]; selectedBeingId=null;
        $("being-empty").hidden=false; $("being-card").hidden=true;
        const list=$("being-list"); if(list) list.innerHTML=activeHunts.length?'<div class="lottery-empty">Your Being is in Game custody during an active Hunt. Resolve it in the Play Console to return it.</div>':'<div class="lottery-empty">No Eternal Beings are currently held by this wallet.</div>';
        setText("being-empty-title",activeHunts.length?"BEING IS HUNTING":"NO BEING HELD");
        setText("being-empty-copy",activeHunts.length?"The NFT has not disappeared. It is temporarily owned by the Game contract and remains linked to your Hunt record.":"Mint, claim, or receive an Eternal Being transfer. Any NFT currently owned by this wallet will appear here.");
        renderPlayConsole();
        void loadSecondaryPlayerState();
        return;
      }
      const hintedTokens=configuredTokenHints();
      if(hintedTokens.length===balance){
        const hintedSummaries=await Promise.all(hintedTokens.map(id=>readBeingSummary(BigInt(id))));
        ownedSummaries=hintedSummaries.filter(summary=>summary.owner.toLowerCase()===account.toLowerCase());
        ownedTokens=ownedSummaries.map(summary=>summary.tokenId);
      }else{
        ownedTokens=await findOwnedTokens(balance);
        ownedSummaries=await Promise.all(ownedTokens.map(readBeingSummary));
      }
      if(ownedTokens.length!==balance) throw new Error("No owned Being token could be verified for this wallet.");
      ownedSummaries.sort((a,b)=>{
        const scoreA=Number(a.being.stage)*100000+Number(a.being.power)*1000+Number(a.being.skill);
        const scoreB=Number(b.being.stage)*100000+Number(b.being.power)*1000+Number(b.being.skill);
        return scoreB-scoreA || Number(b.tokenId-a.tokenId);
      });
      renderBeingList(ownedSummaries,ownedSummaries[0].tokenId);
      await renderBeingDetail(ownedSummaries[0]);
      renderPlayConsole();
      void loadSecondaryPlayerState();
    } catch(err){
      ownedTokens=[]; ownedSummaries=[]; activeHunts=[]; selectedBeingId=null;
      $("being-empty").hidden=false; $("being-card").hidden=true;
      setText("being-empty-title","BEING DATA UNAVAILABLE");setText("being-empty-copy",err.shortMessage||err.message);
      resetPlayConsole();playStatus("Gameplay state unavailable: "+(err.shortMessage||err.message),"error");
    }
  }
  async function loadLotteryHistory() {
    const epochs=await participatingEpochs();
    const commitments=await Promise.all(epochs.map(epoch=>contract.commitments(epoch,account)));
    const participating=epochs.filter((_,i)=>commitments[i]!==zero);
    const rows=await Promise.all(participating.map(async epoch=>{
      const [didReveal,didClaim,count,limit,claimed,seed]=await Promise.all([contract.revealed(epoch,account),contract.claimedEpoch(epoch,account),contract.revealedCount(epoch),contract.epochClaimLimit(epoch),contract.epochClaimedCount(epoch),contract.epochSeed(epoch)]);
      let result="COMMITTED",kind="pending";
      if(didClaim){result="WON / CLAIMED";kind="won";}
      else if(!didReveal){result=epoch===snapshot.e?"WAITING FOR REVEAL":"MISSED REVEAL";kind=epoch===snapshot.e?"pending":"lost";}
      else if(epoch===snapshot.e){result="REVEALED / DRAW PENDING";kind="pending";}
      else if(Number(limit)===0){result="CLAIM CHECK REQUIRED";kind="pending";}
      else if(Number(count)<=Number(limit)){result=Number(claimed)<Number(limit)?"ELIGIBLE — CLAIM NOW":"ALLOCATION FILLED";kind=Number(claimed)<Number(limit)?"won":"lost";}
      else {
        const draw=ethers.solidityPackedKeccak256(["bytes32","address","uint256"],[seed,account,epoch]);
        const chance=(BigInt(limit)*1000000n)/BigInt(count);
        const selected=(BigInt(draw)%1000000n)<chance;
        result=selected?(Number(claimed)<Number(limit)?"DRAW SELECTED — CLAIM NOW":"DRAW SELECTED / FILLED"):"NOT SELECTED — TRY NEXT EPOCH";
        kind=selected&&Number(claimed)<Number(limit)?"won":selected?"pending":"lost";
      }
      return {epoch,result,kind,reveals:Number(count),claimed:Number(claimed),limit:Number(limit)};
    }));
    const box=$("lottery-list"); box.replaceChildren();
    if(!rows.length){box.innerHTML='<div class="lottery-empty">No Mint participation found for this wallet in recent epochs or configured player hints.</div>';return;}
    rows.forEach(row=>{const item=document.createElement("div");item.className="lottery-row "+row.kind;item.innerHTML='<div><b>EPOCH #'+row.epoch+'</b><span>'+row.reveals+' reveals // '+(row.limit||"limit pending")+' slots // '+row.claimed+' claimed</span></div><strong>'+row.result+'</strong>';box.appendChild(item);});
  }
  async function send(label,fn,afterConfirm,simulate,options={}) {
    const report=(message,kind="")=>{status(message,kind);if(options.play)playStatus(message,kind);};
    if(transactionPending) return report("Another wallet transaction is already pending.","warn");
    transactionPending=true;
    try {
      report("Simulating "+label+" against current contract state…");if(simulate)await simulate();
      let gas=null;if(options.estimate){gas=await options.estimate();report(label+" simulation passed // estimated gas "+fmt(gas),"ok");}
      if(options.confirm&&!options.confirm(gas)){report(label+" cancelled. No transaction was sent.","warn");transactionPending=false;return false;}
      report("Confirm "+label+" in your wallet…");
      const tx=await fn();
      report(label+" submitted: "+short(tx.hash));
      await tx.wait();
      report(label+" confirmed.","ok");
      beingIndexCache={};
      transactionPending=false;
      await (afterConfirm?afterConfirm():load());
      return true;
    }
    catch(err){report(label+" failed: "+(err.shortMessage||err.reason||err.message),"error");return false;}
    finally { transactionPending=false; }
  }
  async function commit() {
    if(!walletContract||!snapshot) return;
    try {
      const secret=await deriveWalletSecret(snapshot.e);
      const commitment=ethers.solidityPackedKeccak256(["address","uint256","bytes32"],[account,snapshot.e,secret]);
      saveSecret(snapshot.e,secret,commitment,"wallet-signature-v1");
      await send("Commit",()=>walletContract.commitMint(commitment),null,()=>walletContract.commitMint.staticCall(commitment),{estimate:()=>walletContract.commitMint.estimateGas(commitment)});
    } catch(err){status("Commit preparation failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function reveal() {
    if(!walletContract||!snapshot) return status("Connect the wallet and refresh the current epoch first.","error");
    const [onchainCommitment,alreadyRevealed]=await Promise.all([contract.commitments(snapshot.e,account),contract.revealed(snapshot.e,account)]);
    if(alreadyRevealed)return status("This wallet already revealed in epoch #"+snapshot.e+".","warn");
    if(onchainCommitment===zero)return status("No on-chain commitment exists for this wallet in epoch #"+snapshot.e+".","error");
    try {
      let secret=readSavedSecret(snapshot.e),expected=secret?ethers.solidityPackedKeccak256(["address","uint256","bytes32"],[account,snapshot.e,secret]):null;
      if(expected!==onchainCommitment){secret=await deriveWalletSecret(snapshot.e);expected=ethers.solidityPackedKeccak256(["address","uint256","bytes32"],[account,snapshot.e,secret]);}
      if(expected!==onchainCommitment)return status("Reveal stopped: this is a legacy Commitment and its original secret is unavailable in this browser.","error");
      saveSecret(snapshot.e,secret,onchainCommitment,readSecretRecord(snapshot.e)?.derivation||"wallet-signature-v1");
      await send("Reveal",()=>walletContract.revealMint(snapshot.e,secret),null,()=>walletContract.revealMint.staticCall(snapshot.e,secret),{estimate:()=>walletContract.revealMint.estimateGas(snapshot.e,secret)});
    } catch(err){status("Reveal preparation failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function claim() {
    const epoch=Number($("claim-btn").dataset.epoch);
    if(!walletContract||!Number.isInteger(epoch)||epoch<0) return status("No claimable epoch is selected.","error");
    await send("Claim",()=>walletContract.claimMint(epoch),null,()=>walletContract.claimMint.staticCall(epoch),{estimate:()=>walletContract.claimMint.estimateGas(epoch)});
  }
  async function enterHunt(){
    try{
      if(!walletContract)throw new Error("Connect the owner wallet first.");const id=$("hunt-being-select")?.value,summary=summaryById(id);if(!summary)throw new Error("Select an owned Being.");
      if(Number(summary.cooldown)>snapshot.block)throw new Error("Being #"+id+" remains in cooldown until block "+summary.cooldown+".");
      await send("Enter Hunt for Being #"+id,()=>walletContract.enterHunt(id),async()=>{await load();playStatus("Hunt entered. The Being is visible under Active Hunts and can resolve after at least one new block.","ok");},()=>walletContract.enterHunt.staticCall(id),{play:true,estimate:()=>walletContract.enterHunt.estimateGas(id),confirm:gas=>window.confirm("Ethereum Mainnet // Game "+short(cfg.gameContract)+"\nenterHunt("+id+") // estimated gas "+fmt(gas)+"\n\nThe NFT will move into Game contract custody until you resolve the Hunt. Continue?")});
    }catch(err){playStatus("Hunt entry failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function resolveHunt(id){
    try{
      if(!walletContract)throw new Error("Connect the Hunt owner wallet first.");const hunt=await contract.hunts(id);if(hunt.owner.toLowerCase()!==account.toLowerCase())throw new Error("This wallet does not own the active Hunt.");if(snapshot.block<=Number(hunt.startBlock))throw new Error("Wait for at least one new block before resolving.");
      await send("Resolve Hunt for Being #"+id,()=>walletContract.resolveHunt(id),async()=>{await load();playStatus("Hunt resolved. The Being has returned to your wallet and the result is on-chain.","ok");},()=>walletContract.resolveHunt.staticCall(id),{play:true,estimate:()=>walletContract.resolveHunt.estimateGas(id)});
    }catch(err){playStatus("Hunt resolution failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function fuseSelected(){
    try{
      if(!walletContract)throw new Error("Connect the owner wallet first.");const parent=$("fusion-parent-select")?.value,sacrifice=$("fusion-sacrifice-select")?.value;if(!parent||!sacrifice||parent===sacrifice)throw new Error("Choose two different owned Beings.");if(!$("fusion-confirm")?.checked)throw new Error("Acknowledge the permanent burn first.");
      const parentState=summaryById(parent),sacrificeState=summaryById(sacrifice);if(!parentState||!sacrificeState)throw new Error("Both Beings must still be owned by this wallet.");if(Number(parentState.cooldown)>snapshot.block||Number(sacrificeState.cooldown)>snapshot.block)throw new Error("Both evolution cooldowns must be ready.");
      await send("Fuse #"+parent+" + burn #"+sacrifice,()=>walletContract.fuseBeing(parent,sacrifice),async()=>{await load();playStatus("Fusion confirmed. The parent metadata was refreshed and the sacrifice was burned.","ok");},()=>walletContract.fuseBeing.staticCall(parent,sacrifice),{play:true,estimate:()=>walletContract.fuseBeing.estimateGas(parent,sacrifice),confirm:gas=>window.confirm("Ethereum Mainnet // Game "+short(cfg.gameContract)+"\nfuseBeing(parent #"+parent+", sacrifice #"+sacrifice+") // estimated gas "+fmt(gas)+"\n\nFINAL WARNING: Being #"+sacrifice+" will be permanently burned and cannot be recovered. Continue?")});
    }catch(err){playStatus("Fusion failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  function parseDevourReference(updateInput=false){
    const raw=($("devour-contract")?.value||"").trim(),explicit=($("devour-token-id")?.value||"").trim(),addressMatch=raw.match(/0x[0-9a-fA-F]{40}/);
    if(!addressMatch)return {address:"",tokenText:explicit};
    const tail=raw.slice((addressMatch.index||0)+addressMatch[0].length),embedded=tail.match(/(?:\/|#|:)(\d+)(?:\/?(?:[?#].*)?)?$/),tokenText=explicit||(embedded?.[1]||"");
    if(updateInput&&tokenText&&!explicit&&$("devour-token-id"))$("devour-token-id").value=tokenText;
    return {address:addressMatch[0],tokenText};
  }
  function devourDecision(state){
    if(!state)return {action:"inspect",label:"CHECK NFT",irreversible:false};
    if(state.kind==="punk"){
      if(state.punkLocked&&state.observed&&!state.observationValid)return {action:"blocked",label:"STOP — OBSERVATION EXPIRED",irreversible:false};
      if(state.needsObservation)return {action:"observe",label:"START SAFETY CHECK",irreversible:false};
      if(!state.ownerOk&&!state.punkLocked)return {action:"blocked",label:"CONNECT NFT OWNER WALLET",irreversible:false};
      if(!state.cooldownReady)return {action:"inspect",label:"REFRESH COOLDOWN STATUS",irreversible:false};
      if(state.observationValid&&!state.punkLocked)return {action:"punk-transfer",label:"TRANSFER PUNK FOR DEVOUR",irreversible:true};
      if(state.ready)return {action:"execute",label:"DEVOUR NFT PERMANENTLY",irreversible:true};
      return {action:"blocked",label:"NFT NOT READY",irreversible:false};
    }
    if(!state.ownerOk)return {action:"blocked",label:"CONNECT NFT OWNER WALLET",irreversible:false};
    if(!state.canEventuallyDevour)return {action:"blocked",label:"NFT IS NOT ELIGIBLE",irreversible:false};
    if(state.needsObservation)return {action:"observe",label:"START SAFETY CHECK",irreversible:false};
    if(!state.holdReady)return {action:"inspect",label:"REFRESH — WAITING FOR HOLD",irreversible:false};
    if(!state.cooldownReady)return {action:"inspect",label:"REFRESH COOLDOWN STATUS",irreversible:false};
    if(!state.approved)return {action:"approve",label:"APPROVE THIS NFT",irreversible:false};
    if(state.ready)return {action:"execute",label:"DEVOUR NFT PERMANENTLY",irreversible:true};
    return {action:"blocked",label:"NFT NOT READY",irreversible:false};
  }
  function setDevourButtons(state){
    const button=$("devour-next-btn"),confirm=$("devour-confirm"),decision=devourDecision(state);
    if(!button)return decision;
    button.dataset.action=decision.action;button.textContent=decision.label;button.classList.toggle("danger-action",decision.irreversible);
    if(!state){if(confirm){confirm.checked=false;confirm.disabled=true;}const parsed=parseDevourReference(),ready=Boolean(walletContract&&ownedSummaries.length&&ethers.isAddress(parsed.address)&&/^\d+$/.test(parsed.tokenText));button.textContent=ready?"CHECK NFT":"SELECT AN NFT";button.disabled=!ready;return decision;}
    if(confirm){confirm.disabled=!decision.irreversible;if(!decision.irreversible)confirm.checked=false;}
    button.disabled=decision.action==="blocked"||(decision.irreversible&&!confirm?.checked);
    return decision;
  }
  function setDevourReadouts(simple,technical){setText("devour-readout",simple);setText("devour-technical-readout",technical);}
  async function inspectDevour(){
    inspectedDevour=null;setDevourButtons(null);
    try{
      if(!walletContract||!account)throw new Error("Connect the owner wallet first.");const beingId=$("devour-being-select")?.value,being=summaryById(beingId);if(!being)throw new Error("Select an owned target Being.");
      const parsed=parseDevourReference(true),nft=parsed.address,tokenText=parsed.tokenText;if(!ethers.isAddress(nft))throw new Error("Enter a valid collection address or NFT asset link.");if(!/^\d+$/.test(tokenText))throw new Error("Enter a non-negative integer token ID.");const tokenId=BigInt(tokenText),address=ethers.getAddress(nft);
      if(address.toLowerCase()===cfg.gameContract.toLowerCase())throw new Error("Use Fusion for another Eternal Being.");const code=await provider.getCode(address);if(code==="0x")throw new Error("The collection address has no deployed code.");const codeHash=ethers.keccak256(code);
      const index=curatedEntries.findIndex(entry=>entry.address.toLowerCase()===address.toLowerCase()),entry=index>=0?curatedEntries[index]:null,proof=entry&&curatedRootValid?proofForCurated(index):[],tier=entry?Number(entry.tier):0,isNativePunk=address.toLowerCase()==="0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb",kind=isNativePunk?"punk":entry?"curated":"unknown";
      if(kind==="punk"&&!entry)throw new Error("Native CryptoPunk Devour requires a collection included in the deployed Top-100 root.");if((kind==="curated"||kind==="punk")&&!curatedRootValid)throw new Error("Curated registry root verification failed; tiered actions are disabled.");
      const cooldownReady=Number(being.cooldown)<=snapshot.block;let state={beingId,being,address,tokenId,kind,tier,proof,entry,cooldownReady,ownerOk:false,approved:false,observed:false,ready:false,needsObservation:false,canEventuallyDevour:false};
      if(kind==="punk"){
        const punkRead=new ethers.Contract(address,punkAbi,provider),owner=await punkRead.punkIndexToAddress(tokenId),deposit=await contract.cryptoPunkDeposits(address,tokenId),proofOk=await contract.verifyCollectionTier(address,tier,proof),locked=owner.toLowerCase()===cfg.gameContract.toLowerCase(),ownerOk=owner.toLowerCase()===account.toLowerCase(),observed=deposit.owner.toLowerCase()===account.toLowerCase(),expires=observed?Number(deposit.blockNumber)+snapshot.punkObservation:0,notExpired=observed&&snapshot.block<=expires;
        Object.assign(state,{owner,ownerOk,observed,observationValid:notExpired,punkLocked:locked,proofOk,expires,needsObservation:ownerOk&&(!observed||!notExpired),canEventuallyDevour:proofOk&&(ownerOk||(locked&&observed&&notExpired)),ready:proofOk&&locked&&observed&&notExpired&&cooldownReady});
        const decision=devourDecision(state);setDevourReadouts("CryptoPunk #"+tokenId+" was detected automatically.\nNEXT: "+decision.label,"ROUTE // NATIVE CRYPTOPUNK // "+(entry?.name||short(address))+" // TIER "+tier+"\nProof: "+(proofOk?"VERIFIED":"INVALID")+" // Punk owner: "+short(owner)+" // Observation: "+(observed?(notExpired?"VALID THROUGH BLOCK "+expires+" // "+fmt(expires-snapshot.block)+" blocks remaining":locked?"EXPIRED AFTER TRANSFER — CONTRACT CANNOT COMPLETE THIS DEPOSIT":"EXPIRED — OBSERVE AGAIN") : "REQUIRED")+"\nAutomatic calls: observeCryptoPunk → transferPunk → devourCryptoPunk.");
      }else{
        const nftRead=new ethers.Contract(address,externalNftAbi,provider),[owner,approvedTo,approvedForAll]=await Promise.all([nftRead.ownerOf(tokenId),nftRead.getApproved(tokenId).catch(()=>ethers.ZeroAddress),nftRead.isApprovedForAll(account,cfg.gameContract).catch(()=>false)]),ownerOk=owner.toLowerCase()===account.toLowerCase(),exactApproval=approvedTo.toLowerCase()===cfg.gameContract.toLowerCase(),approved=exactApproval||approvedForAll,approvalType=exactApproval?"EXACT TOKEN":approvedForAll?"BROAD OPERATOR":"REQUIRED";let proofOk=true,observed=true,codeUnchanged=true,holdReady=true,readyBlock=0,supply=null,quota=null;
        if(kind==="curated")proofOk=await contract.verifyCollectionTier(address,tier,proof);
        else{const [observation,totalSupply,used,minimum,limit]=await Promise.all([contract.externalObservations(address,tokenId),nftRead.totalSupply().catch(()=>0n),contract.unknownCollectionDevours(address),contract.UNKNOWN_MIN_TOTAL_SUPPLY(),contract.UNKNOWN_COLLECTION_DEVOUR_LIMIT()]);observed=observation.owner.toLowerCase()===account.toLowerCase();codeUnchanged=!observed||observation.codehash.toLowerCase()===codeHash.toLowerCase();readyBlock=observed?Number(observation.blockNumber)+snapshot.unknownHold:0;holdReady=observed&&codeUnchanged&&snapshot.block>=readyBlock;supply=Number(totalSupply);quota={used:Number(used),limit:Number(limit),minimum:Number(minimum)};}
        const unknownEligible=kind!=="unknown"||(supply>=quota.minimum&&quota.used<quota.limit),needsObservation=kind==="unknown"&&ownerOk&&unknownEligible&&(!observed||!codeUnchanged);Object.assign(state,{owner,ownerOk,approved,approvalType,proofOk,observed,codeUnchanged,holdReady,readyBlock,supply,quota,needsObservation,canEventuallyDevour:ownerOk&&proofOk&&unknownEligible,ready:ownerOk&&approved&&proofOk&&unknownEligible&&observed&&codeUnchanged&&holdReady&&cooldownReady});
        const nutrition=await contract.nutritionForTier(tier),decision=devourDecision(state);setDevourReadouts((kind==="curated"?entry.name+" #"+tokenId+" was detected as Top-100 Tier "+tier+".":"NFT #"+tokenId+" was detected as a standard ERC-721 outside the Top-100 list.")+"\nNEXT: "+decision.label,(kind==="curated"?"ROUTE // CURATED ERC-721 // "+entry.name+" // TIER "+tier:"ROUTE // UNKNOWN ERC-721 // TIER 0")+"\nOwner: "+short(owner)+" // Approval: "+approvalType+(approvedForAll?" (revoke broad approval after Devour)":"")+" // "+(kind==="curated"?"Proof: "+(proofOk?"VERIFIED":"INVALID"):"Observation: "+(!codeUnchanged?"CODE CHANGED — OBSERVE AGAIN":observed?(holdReady?"HOLD COMPLETE":"READY AT BLOCK "+readyBlock):"REQUIRED")+" // Supply "+fmt(supply)+" / minimum "+fmt(quota.minimum)+" // collection uses "+quota.used+" / "+quota.limit)+"\nBase gain: +"+nutrition.nutrition+" Mass / +"+nutrition.complexityGain+" Complexity. Automatic call: "+(kind==="curated"?"approve → devourTieredExternal":"observeExternal → wait → approve → devourExternal")+".");
      }
      inspectedDevour=state;setDevourButtons(state);playStatus("External asset inspected at block "+fmt(snapshot.block)+". Review every line before signing.",state.ready?"ok":"warn");
    }catch(err){setDevourReadouts("NFT CHECK FAILED\n"+(err.shortMessage||err.reason||err.message),"No contract call was prepared.");playStatus("Devour inspection failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function observeDevour(){
    const s=inspectedDevour;if(!s)return;try{if(s.kind==="punk")await send("Observe CryptoPunk #"+s.tokenId,()=>walletContract.observeCryptoPunk(s.address,s.tokenId,s.tier,s.proof),inspectDevour,()=>walletContract.observeCryptoPunk.staticCall(s.address,s.tokenId,s.tier,s.proof),{play:true,estimate:()=>walletContract.observeCryptoPunk.estimateGas(s.address,s.tokenId,s.tier,s.proof)});else await send("Observe external NFT #"+s.tokenId,()=>walletContract.observeExternal(s.address,s.tokenId),inspectDevour,()=>walletContract.observeExternal.staticCall(s.address,s.tokenId),{play:true,estimate:()=>walletContract.observeExternal.estimateGas(s.address,s.tokenId)});}catch(err){playStatus("Observation failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function approveDevour(){
    const s=inspectedDevour;if(!s||s.kind==="punk")return;try{const signer=await new ethers.BrowserProvider(injectedProvider).getSigner(),nftWrite=new ethers.Contract(s.address,externalNftAbi,signer);await send("Approve exact NFT #"+s.tokenId,()=>nftWrite.approve(cfg.gameContract,s.tokenId),inspectDevour,()=>nftWrite.approve.staticCall(cfg.gameContract,s.tokenId),{play:true,estimate:()=>nftWrite.approve.estimateGas(cfg.gameContract,s.tokenId),confirm:gas=>window.confirm("Ethereum Mainnet // NFT "+short(s.address)+"\napprove(Game "+short(cfg.gameContract)+", token #"+s.tokenId+") // estimated gas "+fmt(gas)+"\n\nThis exact approval allows the Game to transfer and permanently lock this NFT only when you later confirm Devour. Continue?")});}catch(err){playStatus("Approval failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function transferPunkForDevour(){
    const s=inspectedDevour;if(!s||s.kind!=="punk")return;try{if(!$("devour-confirm")?.checked)throw new Error("Acknowledge the permanent lock before transferring the Punk.");const signer=await new ethers.BrowserProvider(injectedProvider).getSigner(),punkWrite=new ethers.Contract(s.address,punkAbi,signer);await send("Transfer Punk #"+s.tokenId+" to Game",()=>punkWrite.transferPunk(cfg.gameContract,s.tokenId),inspectDevour,()=>punkWrite.transferPunk.staticCall(cfg.gameContract,s.tokenId),{play:true,estimate:()=>punkWrite.transferPunk.estimateGas(cfg.gameContract,s.tokenId),confirm:gas=>window.confirm("Ethereum Mainnet // CryptoPunk contract "+short(s.address)+"\ntransferPunk(Game "+short(cfg.gameContract)+", #"+s.tokenId+") // estimated gas "+fmt(gas)+"\nObservation expires at block "+s.expires+".\n\nFINAL WARNING: The Punk will enter a Game contract with no rescue or withdrawal path. If Devour is not completed before expiry, it may remain locked without a completion path. Continue?")});}catch(err){playStatus("Punk transfer failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function executeDevour(){
    const s=inspectedDevour;if(!s)return;try{if(!$("devour-confirm")?.checked)throw new Error("Acknowledge the permanent lock first.");if(!s.ready)throw new Error("Re-inspect the asset; all prerequisites are not ready.");
      const fn=s.kind==="punk"?()=>walletContract.devourCryptoPunk(s.beingId,s.address,s.tokenId):s.kind==="curated"?()=>walletContract.devourTieredExternal(s.beingId,s.address,s.tokenId,s.tier,s.proof):()=>walletContract.devourExternal(s.beingId,s.address,s.tokenId),sim=s.kind==="punk"?()=>walletContract.devourCryptoPunk.staticCall(s.beingId,s.address,s.tokenId):s.kind==="curated"?()=>walletContract.devourTieredExternal.staticCall(s.beingId,s.address,s.tokenId,s.tier,s.proof):()=>walletContract.devourExternal.staticCall(s.beingId,s.address,s.tokenId);
      const estimate=s.kind==="punk"?()=>walletContract.devourCryptoPunk.estimateGas(s.beingId,s.address,s.tokenId):s.kind==="curated"?()=>walletContract.devourTieredExternal.estimateGas(s.beingId,s.address,s.tokenId,s.tier,s.proof):()=>walletContract.devourExternal.estimateGas(s.beingId,s.address,s.tokenId);
      await send("Permanent Devour",fn,async()=>{await load();playStatus("Devour confirmed. The external NFT is permanently locked and the Being metadata was refreshed.","ok");},sim,{play:true,estimate,confirm:gas=>window.confirm("Ethereum Mainnet // Game "+short(cfg.gameContract)+"\n"+(s.kind==="punk"?"devourCryptoPunk":"devour"+(s.kind==="curated"?"TieredExternal":"External"))+"(Being #"+s.beingId+", asset "+short(s.address)+" #"+s.tokenId+") // estimated gas "+fmt(gas)+"\n\nFINAL WARNING: This external asset will be locked forever. There is no withdrawal or undo. Continue?")});
    }catch(err){playStatus("Devour failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  async function handleDevourNext(){
    const action=$("devour-next-btn")?.dataset.action||"inspect";
    if(action==="inspect")return inspectDevour();
    if(action==="observe")return observeDevour();
    if(action==="approve")return approveDevour();
    if(action==="punk-transfer")return transferPunkForDevour();
    if(action==="execute")return executeDevour();
  }
  async function transferSelectedBeing() {
    try {
      if(!walletContract||!account) throw new Error("Connect the owner wallet first.");
      if(selectedBeingId===null) throw new Error("Select a Being first.");
      const input=$("transfer-to");
      const to=(input?.value||"").trim();
      if(!ethers.isAddress(to)) throw new Error("Enter a valid recipient address.");
      if(to.toLowerCase()===ethers.ZeroAddress.toLowerCase()) throw new Error("The zero address cannot receive an ERC-721 transfer.");
      if(to.toLowerCase()===account.toLowerCase()) throw new Error("Recipient is already the connected owner.");
      const tokenId=selectedBeingId;
      const [owner,hunt]=await Promise.all([contract.ownerOf(tokenId),contract.hunts(tokenId)]);
      if(owner.toLowerCase()!==account.toLowerCase()) throw new Error("Connected wallet is not the owner of Being #"+tokenId+".");
      if(hunt.owner!==ethers.ZeroAddress) throw new Error("Being #"+tokenId+" is hunting and cannot be transferred now.");
      const transferred=await send("Transfer Being #"+tokenId,()=>walletContract["safeTransferFrom(address,address,uint256)"](account,to,tokenId),async()=>{
        status("Transfer confirmed. Refreshing your Beings…","ok");
        await load();
      },()=>walletContract["safeTransferFrom(address,address,uint256)"].staticCall(account,to,tokenId),{estimate:()=>walletContract["safeTransferFrom(address,address,uint256)"].estimateGas(account,to,tokenId),confirm:gas=>window.confirm("Ethereum Mainnet // Being NFT "+short(cfg.gameContract)+"\nsafeTransferFrom("+short(account)+", "+short(to)+", #"+tokenId+") // estimated gas "+fmt(gas)+"\n\nThis transfers the NFT to the recipient wallet. Verify the address carefully. Continue?")});
      if(transferred&&input) input.value="";
    } catch(err){status("Transfer failed: "+(err.shortMessage||err.reason||err.message),"error");}
  }
  function disconnect() {
    stayDisconnected=true;
    resetPlayerUi("Wallet disconnected from this page.");
  }
  async function handleAccountsChanged(accounts) {
    if(isConnecting) return;
    if(!accounts?.length) return resetPlayerUi("Wallet access was removed or the wallet was locked.");
    if(stayDisconnected) return;
    if(!account||accounts[0].toLowerCase()!==account.toLowerCase()) {
      resetPlayerUi("Wallet account changed. Reconnecting the selected account…");
      await connect();
    }
  }
  async function handleChainChanged(chainId) {
    const next=Number(chainId);
    if(next!==Number(cfg.chainId)) {
      resetPlayerUi("Wallet network changed. Switch back to "+cfg.networkName+" to continue.");
      status("Wrong network. Expected chain "+cfg.chainId+", received chain "+next+".","error");
      return;
    }
    provider=null; contract=null; indexProvider=null; indexContract=null; eventProvider=null; eventContract=null; walletContract=null;
    status("Network changed. Refreshing the configured instance…");
    await load();
    if(injectedProvider&&!stayDisconnected) await connect();
  }
  window.addEventListener("DOMContentLoaded",()=>{
    const devourNode=document.querySelector('.guide-node[data-branch="devour"]'); if(devourNode&&!devourNode.querySelector("a")){const link=document.createElement("a");link.href="devour.html";link.className="btn";link.textContent="VIEW TIERS + TOP 100 →";link.style.marginTop="12px";devourNode.appendChild(link);}
    setExplorerLinks();
    $("connect-btn").addEventListener("click",connect);
    const disconnectBtn=$("disconnect-btn"); if(disconnectBtn) disconnectBtn.addEventListener("click",disconnect);
    $("refresh-btn").addEventListener("click",load);
    $("commit-btn").addEventListener("click",commit);
    $("reveal-btn").addEventListener("click",reveal);
    $("claim-btn").addEventListener("click",claim);
    const transferBtn=$("transfer-btn"); if(transferBtn) transferBtn.addEventListener("click",transferSelectedBeing);
    $("hunt-being-select")?.addEventListener("change",updateHuntReadout);
    $("hunt-enter-btn")?.addEventListener("click",enterHunt);
    $("fusion-parent-select")?.addEventListener("change",updateFusionPreview);
    $("fusion-sacrifice-select")?.addEventListener("change",updateFusionPreview);
    $("fusion-confirm")?.addEventListener("change",updateFusionPreview);
    $("fusion-btn")?.addEventListener("click",fuseSelected);
    $("devour-next-btn")?.addEventListener("click",handleDevourNext);
    $("devour-confirm")?.addEventListener("change",()=>setDevourButtons(inspectedDevour));
    $("devour-wallet-nft-select")?.addEventListener("change",selectWalletNft);
    $("devour-nft-refresh-btn")?.addEventListener("click",()=>loadWalletNfts({force:true}));
    $("devour-nft-more-btn")?.addEventListener("click",()=>loadWalletNfts({append:true}));
    ["devour-contract","devour-token-id"].forEach(id=>{const element=$(id);element?.addEventListener("input",()=>{const picker=$("devour-wallet-nft-select");if(picker)picker.value="";renderSelectedWalletNft(null);inspectedDevour=null;setDevourButtons(null);setDevourReadouts("Manual NFT reference changed. Check the NFT to continue.","The automatic contract route will be recalculated from current on-chain state.");});});
    $("devour-being-select")?.addEventListener("change",()=>{inspectedDevour=null;setDevourButtons(null);setDevourReadouts("Target Being changed. Check the selected NFT again.","The automatic contract route will be recalculated from current on-chain state.");});
    const injected=walletProvider();
    if(injected?.on){
      injected.on("accountsChanged",handleAccountsChanged);
      injected.on("chainChanged",handleChainChanged);
    }
    setGuide("connect");
    resetPlayConsole();
    load();
    setInterval(load,Math.max(8,Number(cfg.refreshSeconds||12))*1000);
  });
})();
