bend_puzzle/app.js
33333-33333 9d70afb4cc
Some checks failed
BEND FIELD CI / release (push) Has been cancelled
BEND FIELD CI / production-bridge (push) Has been cancelled
t
2026-08-01 22:31:04 +09:00

6600 lines
614 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const BuildMeta=globalThis.BendBuildMeta;
if(!BuildMeta)throw new Error('BendBuildMeta is not loaded');
const SharedContracts=globalThis.BendSharedContracts;
if(!SharedContracts)throw new Error('BendSharedContracts is not loaded');
const CanonicalStoreCatalog=globalThis.BendStoreCatalog;
if(!Array.isArray(CanonicalStoreCatalog))throw new Error('BendStoreCatalog is not loaded');
const AppLogic=globalThis.BendAppLogic;
if(!AppLogic)throw new Error('BendAppLogic is not loaded');
const {key2,ckey,sameCell}=globalThis.BendPuzzle;
const FieldPersistence=globalThis.BendFieldPersistence;
if(!FieldPersistence)throw new Error('BendFieldPersistence is not loaded');
const FrameSchedulerApi=globalThis.BendFrameScheduler;
if(!FrameSchedulerApi)throw new Error('BendFrameScheduler is not loaded');
const DragSchedulerApi=globalThis.BendDragScheduler;
if(!DragSchedulerApi)throw new Error('BendDragScheduler is not loaded');
const InteractionStateApi=globalThis.BendInteractionState;
if(!InteractionStateApi)throw new Error('BendInteractionState is not loaded');
const interactionState=InteractionStateApi.createInteractionState();
function interactionActive(scope='any'){return interactionState.active(scope)}
const GestureCoordinatorApi=globalThis.BendGestureCoordinator;
if(!GestureCoordinatorApi)throw new Error('BendGestureCoordinator is not loaded');
const gestureCoordinator=GestureCoordinatorApi.createGestureCoordinator();
const CursorModelApi=globalThis.BendCursorModel;
if(!CursorModelApi)throw new Error('BendCursorModel is not loaded');
const sectionCountRange=AppLogic.sectionCountRange,normalizeGeneratedShape=AppLogic.normalizeGeneratedShape,generatedShapeKey=AppLogic.generatedShapeKey,generatedShapeFamilyKey=AppLogic.generatedShapeFamilyKey,balancedShapeCandidates=AppLogic.balancedShapeCandidates,growConnectedShape=AppLogic.growConnectedShape,lineStrokeWidth=AppLogic.lineStrokeWidth;
const{APP_VERSION,SAVE_SCHEMA,STORAGE_SCHEMA,IDB_LAYOUT_VERSION,FIELD_STORAGE_FORMAT,GAMEPLAY_DATA_VERSION,WORLD_GENERATION,GENERATOR_VERSION}=BuildMeta;
const CELL=43,CHUNK=5,UNIT=CELL*CHUNK,PAD=26;
const MAX_SCORE=Number.MAX_SAFE_INTEGER,MAX_SOLVES=1000000000,MAX_WORLD_COORD=1000000000;
const normalizeSpecialMechanics=SharedContracts.normalizeSpecialMechanics;
const appVersionLabel=`v${APP_VERSION}`,brandVersion=document.querySelector('.brand small');
if(brandVersion)brandVersion.textContent=appVersionLabel;
if(!document.title.endsWith(appVersionLabel))document.title=`${document.title.replace(/\s+v[\w.-]+$/,'').trim()} ${appVersionLabel}`;
const MAX_BOARDS=200000,MAX_PATHS_PER_BOARD=512,MAX_IMPORT_BYTES=100*1024*1024,CLOCK_DRIFT_LIMIT=5*60*1000,LOCAL_MIRROR_MAX_BYTES=4.5*1024*1024,IDB_STARTUP_TIMEOUT=8000,MAX_FRONTIER_GENERATION_CYCLES=3,GC_BATCH_ROWS=500;
const MAX_RENDER_FPS=30,GLOBAL_FRAME_INTERVAL=1000/MAX_RENDER_FPS,AUXILIARY_FPS=30,AUXILIARY_FRAME_INTERVAL=1000/AUXILIARY_FPS,REACTION_TARGET_FPS=30,REACTION_FRAME_INTERVAL=1000/REACTION_TARGET_FPS,DRAG_TARGET_FPS=30,DRAG_FRAME_INTERVAL=1000/DRAG_TARGET_FPS,INTERACTION_FRAME_TOLERANCE_MS=1.25,DRAG_DISPLAY_WATCHDOG_MS=34,CAMERA_DISPLAY_WATCHDOG_MS=34,DRAG_MAX_POINTER_SAMPLES=12,DRAG_MAX_LOGICAL_SAMPLES_PER_FRAME=4,DRAG_MODEL_BUDGET_MS=.5,DRAG_MAX_LIVE_CATCHUP_CELLS=6,DRAG_MAX_RELEASE_CATCHUP_CELLS=24,LIGHTWEIGHT_DRAG_BOARD_CELLS=120,GENERATION_FAILURE_BONUS=2500,GENERATION_FAILURE_MIN_MS=8000,SHARED_EXPANSION_GRACE_MS=60000,SHARED_EXPANSION_JITTER_MS=30000,REALTIME_CURSOR_INTERVAL=50,PHP_REALTIME_CURSOR_INTERVAL=250,PHP_REALTIME_POLL_INTERVAL=500,REALTIME_CURSOR_HEARTBEAT_INTERVAL=5000,REALTIME_VIEWPORT_INTERVAL=250,REALTIME_PLAYER_STALE_MS=15000,REALTIME_CLAIM_REQUEST_TIMEOUT=5000,REALTIME_CLAIM_TOUCH_INTERVAL=20000,REALTIME_REACTION_DURATION=4500,REALTIME_REACTION_RATE_INTERVAL=500,REACTION_LONG_PRESS_MS=500,REACTION_MOVE_CANCEL_PX=18;
const INTERACTION_SCHEDULER_VARIANT=(()=>{try{return localStorage.getItem('bend-field-interaction-scheduler-variant')==='reduced'?'reduced':'full'}catch(_){return'full'}})();
globalThis.BEND_INTERACTION_SCHEDULER=Object.freeze({enabled:true,version:2,variant:INTERACTION_SCHEDULER_VARIANT});
const LEGACY_LOCAL_SOLVER='\u3042\u306a\u305f',DEFAULT_PLAYER_NAME='\u65c5\u4eba';
const AUTO_NAME_ADJECTIVES=Object.freeze(['\u9752\u3044','\u8d64\u3044','\u767d\u3044','\u9ed2\u3044','\u91d1\u8272\u306e','\u9280\u8272\u306e','\u9759\u304b\u306a','\u7d20\u65e9\u3044','\u3084\u3055\u3057\u3044','\u3075\u3057\u304e\u306a']);
const AUTO_NAME_NOUNS=Object.freeze(['\u30ad\u30c4\u30cd','\u30cd\u30b3','\u30d5\u30af\u30ed\u30a6','\u30da\u30f3\u30ae\u30f3','\u30e9\u30c3\u30b3','\u30ab\u30e1','\u30af\u30b8\u30e9','\u30cf\u30ea\u30cd\u30ba\u30df','\u30ab\u30ef\u30a6\u30bd','\u30ed\u30dc\u30c3\u30c8']);
function createAutomaticPlayerName(){const values=new Uint32Array(2);if(globalThis.crypto?.getRandomValues)globalThis.crypto.getRandomValues(values);else{values[0]=(Math.random()*0xffffffff)>>>0;values[1]=(Math.random()*0xffffffff)>>>0}return`${AUTO_NAME_ADJECTIVES[values[0]%AUTO_NAME_ADJECTIVES.length]}${AUTO_NAME_NOUNS[values[1]%AUTO_NAME_NOUNS.length]}-${String(((values[0]^values[1])>>>0)%1000).padStart(3,'0')}`}
function currentPlayerName(){return typeof data?.playerName==='string'&&data.playerName.trim()?data.playerName.trim().slice(0,24):DEFAULT_PLAYER_NAME}
function currentPlayerId(){return typeof data?.cloudProfile?.playerId==='string'?data.cloudProfile.playerId:null}
const STARTER_SEED=0x9c37a5e1;
const STARTER_PUZZLE=Object.freeze({"g":[[0,0,"W"],[0,1,"N"],[1,4,"E"],[0,4,"E"],[4,0,"W"],[4,3,"S"],[2,4,"E"],[4,4,"E"],[4,1,"S"],[4,2,"S"]],"n":[[1,0,3],[1,2,2],[2,0,3],[4,4,2],[3,1,2]],"valid":[[0,0],[0,1],[0,2],[0,3],[0,4],[1,0],[1,1],[1,2],[1,3],[1,4],[2,0],[2,1],[2,2],[2,3],[2,4],[3,0],[3,1],[3,2],[3,3],[3,4],[4,0],[4,1],[4,2],[4,3],[4,4]],"bounds":{"w":5,"h":5},"axis":"MIX","solution":[{"startGate":0,"endGate":1,"cells":[[0,0],[1,0],[1,1],[0,1]]},{"startGate":2,"endGate":3,"cells":[[1,4],[1,3],[1,2],[0,2],[0,3],[0,4]]},{"startGate":4,"endGate":5,"cells":[[4,0],[3,0],[2,0],[2,1],[2,2],[2,3],[3,3],[4,3]]},{"startGate":6,"endGate":7,"cells":[[2,4],[3,4],[4,4]]},{"startGate":8,"endGate":9,"cells":[[4,1],[3,1],[3,2],[4,2]]}],"level":1,"maxTurns":3,"totalTurns":12,"style":"variable-world-gates","complexity":{"rating":1,"score":1,"areaUnits":1,"lineCount":5,"totalTurns":12,"avgTurns":2.4,"maxTurns":3,"avgLength":5,"maxLength":8},"difficulty":1,"regionalTarget":1});
const SCORE_VERSION=6,STORE_PRICE_VERSION=1,MIN_STORE_ITEM_PRICE=3000,MIN_CURSOR_PRICE=500,MAX_FACE_CURSOR_PRICE=50000,STORE_CHANCE=1/10,DRAG_FLAG_CURSOR_SIZE=12.4,DRAG_FLAG_CLIP_RADIUS=5.8,MINIMAP_LONG_LINE=1024,MINIMAP_VIEW_CHUNKS_X=42;
const MIN_CAMERA_SCALE=.08,SCORE_LENS_ZOOM_THRESHOLD=.72,OVERVIEW_ZOOM_THRESHOLD=.54,SOUND_GAIN_MULTIPLIER=5.2,UNIQUE_SOLUTION_MIN_LEVEL=6;
const SPECIAL_CELL_MIN_LEVEL=5;
const DRAG_EDGE_MARGIN=76,DRAG_EDGE_MAX_SPEED=.58;
const POINTER_SNAP_THRESHOLD=CELL*.45,POINTER_DOMINANT_RATIO=1.25;
const TIME_ATTACK_MINUTES=Object.freeze([3,5,10]);
const REACTION_EMOJIS=Object.freeze(['👍','🤩','🙏','🧠','🎉']);
const DEBUG_PURCHASE_MODE=location.pathname==='/debug-items'||location.pathname.endsWith('/debug-items')||new URLSearchParams(location.search).get('debug')==='items';
const TIME_ATTACK_COOLDOWN_MINUTES=Object.freeze({3:10,5:15,10:20});
const TIME_ATTACK_TIERS=Object.freeze([
Object.freeze({score:1000,multiplier:3}),
Object.freeze({score:500,multiplier:2.5}),
Object.freeze({score:250,multiplier:2}),
Object.freeze({score:100,multiplier:1.5}),
Object.freeze({score:0,multiplier:1.25})
]);
const COSMETIC_PRESENTATIONS=Object.freeze([
['line-color-cyan','スカイシアン','●','ラインカラー','澄んだ電気色のシアン。'],
['line-color-gold','ソーラーゴールド','●','ラインカラー','宝石のように明るい金色。'],['line-color-mint','ミントシグナル','●','ラインカラー','くっきりした信号色のグリーン。'],
['line-color-violet','アーケードバイオレット','●','ラインカラー','深みのある紫色。'],['line-color-tangerine','タンジェリン','●','ラインカラー','温かみのあるオレンジ。'],
['line-color-cobalt','コバルト','●','ラインカラー','彩度の高いブルー。'],['line-color-coral','ホットコーラル','●','ラインカラー','明るいコーラルレッド。'],
['line-color-aqua','アクアパルス','●','ラインカラー','涼しげな青緑色。'],['line-color-magenta','マゼンタポップ','●','ラインカラー','力強いマゼンタ。'],
['line-color-pearl','パールホワイト','●','プレミアムラインカラー','明るい真珠色のホワイト。'],['line-color-lime','ハイパーライム','●','プレミアムラインカラー','高エネルギーなライム色。'],
['line-color-amber','アンバーコア','●','プレミアムラインカラー','濃密な琥珀色。'],
['line-color-ice','アークティックアイス','●','プレミアムラインカラー','淡く結晶感のある水色。'],['line-color-lavender','ドリームラベンダー','●','プレミアムラインカラー','柔らかなラベンダー色。'],
['line-effect-aurora','オーロラ','≋','ショップ限定ラインカラー','選定されたオーロラ色が2秒ごとに切り替わり、線とゲートを彩ります。'],
['reaction-effect-giant','巨大','😀','リアクション','巨大な絵文字が落下し、盤面を揺らして背景にひびを入れます。'],
['reaction-effect-laser','レーザー','⚡','リアクション','大量のレーザーが交差するディスコ風エフェクト。'],['reaction-effect-orbit','オービット','🪐','リアクション','傾いた巨大な土星と星、周回する絵文字を表示します。'],
['reaction-effect-firework','花火','🎆','リアクション','打ち上げ後に多重の絵文字リングと小花火が開きます。'],
['reaction-effect-comet','彗星','☄','リアクション','遠方から絵文字が突入し、振動して大爆発します。']
].map(([id,name,icon,effectLabel,description])=>Object.freeze({id,name,icon,effectLabel,toast:name,description})));
const STORE_ITEM_BASE=Object.freeze([
Object.freeze({id:'score-lens',name:'ジェムレンズ',icon:'▦',effectLabel:'予想報酬表示 オン/オフ',toast:'予想報酬表示を切り替え',description:'未クリア盤面の予想報酬表示を切り替えます。'}),
...COSMETIC_PRESENTATIONS
]);
const YELLOW_FACE_CURSOR_SOURCE=`1F600|grinning face
1F603|grinning face with big eyes
1F604|grinning face with smiling eyes
1F601|beaming face with smiling eyes
1F606|grinning squinting face
1F605|grinning face with sweat
1F923|rolling on the floor laughing
1F602|face with tears of joy
1F642|slightly smiling face
1F643|upside-down face
1FAE0|melting face
1F609|winking face
1F60A|smiling face with smiling eyes
1F607|smiling face with halo
1F970|smiling face with hearts
1F60D|smiling face with heart-eyes
1F929|star-struck
1F618|face blowing a kiss
1F617|kissing face
263A FE0F|smiling face
1F61A|kissing face with closed eyes
1F619|kissing face with smiling eyes
1F972|smiling face with tear
1F60B|face savoring food
1F61B|face with tongue
1F61C|winking face with tongue
1F92A|zany face
1F61D|squinting face with tongue
1F911|money-mouth face
1F917|smiling face with open hands
1F92D|face with hand over mouth
1FAE2|face with open eyes and hand over mouth
1FAE3|face with peeking eye
1F92B|shushing face
1F914|thinking face
1FAE1|saluting face
1F910|zipper-mouth face
1F928|face with raised eyebrow
1F610|neutral face
1F611|expressionless face
1F636|face without mouth
1FAE5|dotted line face
1F636 200D 1F32B FE0F|face in clouds
1F60F|smirking face
1F612|unamused face
1F644|face with rolling eyes
1F62C|grimacing face
1F62E 200D 1F4A8|face exhaling
1F925|lying face
1FAE8|shaking face
1F60C|relieved face
1F614|pensive face
1F62A|sleepy face
1F924|drooling face
1F634|sleeping face
1F637|face with medical mask
1F912|face with thermometer
1F915|face with head-bandage
1F927|sneezing face
1F974|woozy face
1F635|face with crossed-out eyes
1F635 200D 1F4AB|face with spiral eyes
1F92F|exploding head
1F920|cowboy hat face
1F973|partying face
1F978|disguised face
1F60E|smiling face with sunglasses
1F913|nerd face
1F9D0|face with monocle
1F615|confused face
1FAE4|face with diagonal mouth
1F61F|worried face
1F641|slightly frowning face
2639 FE0F|frowning face
1F62E|face with open mouth
1F62F|hushed face
1F632|astonished face
1F633|flushed face
1F97A|pleading face
1F979|face holding back tears
1F626|frowning face with open mouth
1F627|anguished face
1F628|fearful face
1F630|anxious face with sweat
1F625|sad but relieved face
1F622|crying face
1F62D|loudly crying face
1F631|face screaming in fear
1F616|confounded face
1F623|persevering face
1F61E|disappointed face
1F613|downcast face with sweat
1F629|weary face
1F62B|tired face
1F971|yawning face
1F624|face with steam from nose
1F620|angry face`;
const YELLOW_FACE_CURSOR_ITEMS=Object.freeze(YELLOW_FACE_CURSOR_SOURCE.split('\n').map((row,index)=>{
const[codes,name]=row.split('|'),codeKey=codes.toLowerCase().replace(/\s+/g,'-'),
emoji=String.fromCodePoint(...codes.split(' ').map(code=>Number.parseInt(code,16)));
return Object.freeze({
id:`cursor-face-${codeKey}`,name:`${emoji} カーソル`,cursorEmoji:emoji,icon:emoji,
effectLabel:'絵文字カーソル',toast:`${emoji} カーソル`,
description:'\u9ec4\u8272\u3044\u8868\u60c5\u7d75\u6587\u5b57\u306e\u30ab\u30fc\u30bd\u30eb\u3067\u3059\u3002'
});
}));
const FLAG_REGION_CODES=`AC AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CP CQ CR CU CV CW CX CY CZ DE DG DJ DK DM DO DZ EA EC EE EG EH ER ES ET EU FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU IC ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TA TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM UN US UY UZ VA VC VE VG VI VN VU WF WS XK YE YT ZA ZM ZW`.split(' ');
function regionFlagEmoji(code){return String.fromCodePoint(...[...code].map(letter=>0x1f1e6+letter.charCodeAt(0)-65))}
const regionDisplayNames=typeof Intl?.DisplayNames==='function'?new Intl.DisplayNames(['ja'],{type:'region'}):null;
function regionFlagName(code){try{return regionDisplayNames?.of(code)||code}catch(_){return code}}
function subdivisionFlagEmoji(tag){return String.fromCodePoint(0x1f3f4,...[...tag].map(letter=>0xe0000+letter.charCodeAt(0)),0xe007f)}
function emojiAssetKey(emoji){return[...emoji].map(character=>character.codePointAt(0).toString(16)).join('-')}
const FLAG_CURSOR_ITEMS=Object.freeze([
...FLAG_REGION_CODES.map(code=>({key:code.toLowerCase(),name:regionFlagName(code),emoji:regionFlagEmoji(code)})),
...[['gbeng','イングランド'],['gbsct','スコットランド'],['gbwls','ウェールズ']].map(([key,name])=>({key,name,emoji:subdivisionFlagEmoji(key)}))
].map(source=>Object.freeze({
id:`cursor-flag-${source.key}`,name:`${source.name}の国旗`,cursorEmoji:source.emoji,icon:source.emoji,flagAsset:`assets/flags/${emojiAssetKey(source.emoji)}.svg`,
effectLabel:'国旗カーソル',toast:source.emoji,description:''
})));
const STORE_PRESENTATION_ITEMS=Object.freeze([...STORE_ITEM_BASE,...YELLOW_FACE_CURSOR_ITEMS,...FLAG_CURSOR_ITEMS]);
const STORE_PRESENTATION_CATALOG=new Map(STORE_PRESENTATION_ITEMS.map(item=>[item.id,item]));
if(STORE_PRESENTATION_CATALOG.size!==CanonicalStoreCatalog.length||STORE_PRESENTATION_ITEMS.some(item=>!CanonicalStoreCatalog.some(contract=>contract.id===item.id)))throw new Error('ショップ表示データがカタログと一致しません');
const STORE_ITEMS=Object.freeze(CanonicalStoreCatalog.map(contract=>{
const presentation=STORE_PRESENTATION_CATALOG.get(contract.id);
if(!presentation)throw new Error(`ショップアイテム ${contract.id} の表示データがありません`);
return Object.freeze({...presentation,...contract});
}));
const CURSOR_ITEMS=Object.freeze(STORE_ITEMS.filter(item=>item.cursorStyle));
const STORE_ITEM_CATALOG=new Map(STORE_ITEMS.map(item=>[item.id,item]));
const cursorModel=CursorModelApi.createCursorModel(CURSOR_ITEMS);
const STORE_ITEM_IDS=new Set(STORE_ITEMS.map(item=>item.id));
const LINE_COLOR_ITEMS=Object.freeze(STORE_ITEMS.filter(item=>item.lineColor)),LINE_COLORS=Object.freeze(LINE_COLOR_ITEMS.map(item=>item.lineColor));
const AURORA_LINE_COLOR_ITEM_ID='line-effect-aurora',STARTER_LINE_COLOR_IDS=Object.freeze(LINE_COLOR_ITEMS.filter(item=>item.effectLabel==='ラインカラー').map(item=>item.id)),LINE_EFFECT_IDS=new Set(['aurora',...STORE_ITEMS.map(item=>item.lineEffect).filter(Boolean)]),REACTION_STYLE_IDS=new Set(['classic',...STORE_ITEMS.map(item=>item.reactionStyle).filter(Boolean)]);
function randomStarterLineColorId(){const value=new Uint32Array(1);try{globalThis.crypto?.getRandomValues?.(value)}catch(_){}if(!value[0])value[0]=(Math.random()*0xffffffff)>>>0;return STARTER_LINE_COLOR_IDS[value[0]%STARTER_LINE_COLOR_IDS.length]}
function validLineColorItemId(value){return typeof value==='string'&&Boolean(STORE_ITEM_CATALOG.get(value)?.lineColor)}
function validStarterLineColorId(value){return typeof value==='string'&&STARTER_LINE_COLOR_IDS.includes(value)}
function migratedLineColorStyle(source){const candidate=source?.lineEffectStyle==='aurora'?AURORA_LINE_COLOR_ITEM_ID:source?.lineColorStyle;return validLineColorItemId(candidate)?candidate:null}
function normalizeEquippedCosmeticsInPlace(target){if(!target)return target;target.lineColorStyle=migratedLineColorStyle(target)||(validStarterLineColorId(target.starterLineColor)?target.starterLineColor:STARTER_LINE_COLOR_IDS[0]);target.lineEffectStyle='none';target.reactionStyle=REACTION_STYLE_IDS.has(target.reactionStyle)?target.reactionStyle:'classic';target.lastReaction=target.lastReaction==='👉🏻'?'🤩':REACTION_EMOJIS.includes(target.lastReaction)?target.lastReaction:'👍';return target}
function starterLineColorForPlayer(playerId){if(typeof playerId!=='string'||!playerId)return randomStarterLineColorId();return STARTER_LINE_COLOR_IDS[hash32(AppLogic.stableHash(['starter-line-color',playerId]))%STARTER_LINE_COLOR_IDS.length]}
const DIFF_BACKGROUND_EASY=['#102b32','#17413f'],DIFF_BACKGROUND_HARD=['#43151f','#68202b'];
const REGIONS=[{name:'\u68ee',accent:'#72e38f'},{name:'\u6f6e',accent:'#5fd8ff'},{name:'\u5bb5',accent:'#a98cff'},{name:'\u706b',accent:'#ff915f'},{name:'\u865a',accent:'#ff709f'}];
const OPP={N:'S',S:'N',W:'E',E:'W'};
const SIDE_D={N:[-1,0],S:[1,0],W:[0,-1],E:[0,1]};
const storageKey=`bend-field:v${STORAGE_SCHEMA}:${WORLD_GENERATION}:compact`,
recoveryStorageKey=`bend-field:v${STORAGE_SCHEMA}:${WORLD_GENERATION}:recovery`,
recoveryJournalKey=`bend-field:v${STORAGE_SCHEMA}:${WORLD_GENERATION}:journal`,
recoveryJournalPrefix=`bend-field:v${STORAGE_SCHEMA}:${WORLD_GENERATION}:journal:`,
storageRevisionKey=`bend-field:v${STORAGE_SCHEMA}:${WORLD_GENERATION}:revision`,
worldEpochStorageKey=`bend-field:v${STORAGE_SCHEMA}:${WORLD_GENERATION}:epoch`,
worldDbName=`bend-field:v${STORAGE_SCHEMA}:${WORLD_GENERATION}:world`,
worldLockName=`bend-field:v${STORAGE_SCHEMA}:${WORLD_GENERATION}:world`,
CHAIN_WINDOW=120000,SAVE_DELAY=90;
const mirrorChunkPrefix=`${storageKey}:chunk:`;
const MIRROR_IDLE_DELAY=5000,MIRROR_IDLE_TIMEOUT=15000,MIRROR_CHUNK_BYTES=64*1024,MIRROR_CHUNK_FORMAT='bend-field-chunked-v1',MINIMAP_CACHE_OVERSCAN_CHUNKS=4,LOD_CHANGES_PER_PASS=1,BOARD_RENDERS_PER_FRAME=6,HYDRATE_CONCURRENCY=4;
const RETIRED_WORLD_STORES=Object.freeze([
Object.freeze({schema:31,generation:'v47-field-reset-20260728-bugfix'}),
Object.freeze({schema:30,generation:'v47-field-reset-20260727'})
]);
const world=document.querySelector('#world'),viewport=document.querySelector('#viewport'),boardHudLayer=document.querySelector('#boardHudLayer'),topbar=document.querySelector('#topbar'),overviewCanvas=document.querySelector('#overviewCanvas'),noiseCanvas=document.querySelector('#noiseCanvas'),
solvedCountEl=document.querySelector('#solvedCount'),scoreCountEl=document.querySelector('#scoreCount'),
worldCountEl=document.querySelector('#worldCount'),fpsCounter=document.querySelector('#fpsCounter'),playerNameBtn=document.querySelector('#playerNameBtn'),settingsBtn=document.querySelector('#settingsBtn'),
minimapCanvas=document.querySelector('#minimapCanvas'),clearFeed=document.querySelector('#clearFeed'),presenceCanvas=document.querySelector('#presenceCanvas'),reactionCanvas=document.querySelector('#reactionCanvas'),reactionRadial=document.querySelector('#reactionRadial'),
saveStatusEl=document.querySelector('#saveStatus'),statusPanel=document.querySelector('#statusPanel'),
statusMessage=document.querySelector('#statusMessage'),specialTooltip=document.querySelector('#specialTooltip');
const timeAttackSuggestion=document.querySelector('#timeAttackSuggestion');
const runtimeConfig=globalThis.BendRuntimeConfig||Object.freeze({}),cloudApiEnabled=runtimeConfig.cloudApi===true,
cloudAppBaseUrl=(()=>{try{return new URL(runtimeConfig.appBaseUrl||'./',document.baseURI).href}catch(_){return document.baseURI}})(),
cloudApiBridgeUrl=(()=>{try{return runtimeConfig.apiBridgeUrl?new URL(runtimeConfig.apiBridgeUrl,cloudAppBaseUrl).href:''}catch(_){return''}})(),
cloudApiBaseCandidates=(()=>{const values=[];for(const candidate of[`${cloudAppBaseUrl}api/`,cloudApiBridgeUrl,(()=>{try{return new URL('/api/',location.href).href}catch(_){return'/api/'}})()])if(candidate&&!values.includes(candidate))values.push(candidate);return Object.freeze(values)})();
let cloudApiBaseUrl=cloudApiBaseCandidates[0]||'/api/',cloudApiPhpBridgeActive=false;
function cloudEndpointUrl(value,baseUrl=cloudApiBaseUrl){
if(typeof value!=='string'||!/^\/?api(?:\/|$)/.test(value))return value;
if(typeof cloudApiBridgeUrl!=='undefined'&&cloudApiBridgeUrl&&baseUrl===cloudApiBridgeUrl){
try{const source=new URL(value,'https://linkfield.invalid'),endpoint=new URL(cloudApiBridgeUrl);endpoint.searchParams.set('path',source.pathname);for(const[key,item]of source.searchParams)endpoint.searchParams.append(key,item);return endpoint.href}catch(_){return value}
}
const relative=value.replace(/^\/?api\/?/,'');try{return new URL(relative,baseUrl).href}catch(_){return`/api/${relative}`}
}
function cloudApiUsesPhpBridge(){return cloudApiPhpBridgeActive||Boolean(cloudApiBridgeUrl&&cloudApiBaseUrl===cloudApiBridgeUrl)}
const sessionId=globalThis.crypto?.randomUUID?.()||`session-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const sessionRecoveryJournalKey=recoveryJournalPrefix+sessionId;
const loadNotices=[];
let volatileRecovery=null,storageAccessError=null,serverClockOffset=null;
const sessionWallClock=Date.now(),sessionMonotonic=globalThis.performance?.now?.()||0;
function deepClone(value){if(globalThis.structuredClone)try{return globalThis.structuredClone(value)}catch(_){}return JSON.parse(JSON.stringify(value))}
function sameDataValue(a,b){
if(Object.is(a,b))return true;
if(!a||!b||typeof a!=='object'||typeof b!=='object'||Array.isArray(a)!==Array.isArray(b))return false;
if(Array.isArray(a)){if(a.length!==b.length)return false;for(let index=0;index<a.length;index++)if(!sameDataValue(a[index],b[index]))return false;return true}
const aKeys=Object.keys(a),bKeys=Object.keys(b);if(aKeys.length!==bKeys.length)return false;
for(const key of aKeys)if(!Object.prototype.hasOwnProperty.call(b,key)||!sameDataValue(a[key],b[key]))return false;
return true;
}
function trustedNow(){
const monotonic=sessionWallClock+((globalThis.performance?.now?.()||sessionMonotonic)-sessionMonotonic),wall=Date.now(),serverWall=Number.isFinite(serverClockOffset)?wall+serverClockOffset:null,floor=Number.isFinite(data?.clockFloor)?data.clockFloor:0;
if(serverWall!=null)return Math.max(floor,serverWall);
return Math.max(floor,Math.abs(wall-monotonic)>CLOCK_DRIFT_LIMIT?monotonic:wall);
}
function safeLocalGet(key){try{return localStorage.getItem(key)}catch(error){storageAccessError=error;return null}}
function safeLocalSet(key,value){try{localStorage.setItem(key,value);storageAccessError=null;return true}catch(error){storageAccessError=error;return false}}
function safeLocalRemove(key){try{localStorage.removeItem(key);return true}catch(error){storageAccessError=error;return false}}
const UI_SETTINGS_KEY='bend-field:ui-settings:v1';
function normalizeUiSettings(raw){return{lightweightRendering:raw?.lightweightRendering===true,soundEnabled:raw?.soundEnabled!==false}}
function readUiSettings(){try{return normalizeUiSettings(JSON.parse(safeLocalGet(UI_SETTINGS_KEY)||'{}'))}catch(_){return normalizeUiSettings(null)}}
let uiSettings=readUiSettings();
function applyUiSettings(){document.body.dataset.interactionScheduler=INTERACTION_SCHEDULER_VARIANT;document.body.classList.toggle('lightweight-rendering',uiSettings.lightweightRendering);document.body.classList.toggle('reduced-effects',uiSettings.lightweightRendering);if(!uiSettings.lightweightRendering)scheduleNoiseBackground?.(true)}
function persistUiSettings(){safeLocalSet(UI_SETTINGS_KEY,JSON.stringify(uiSettings));applyUiSettings()}
function safeLocalKeys(prefix=''){
try{const keys=[];for(let index=0;index<localStorage.length;index++){const key=localStorage.key(index);if(typeof key==='string'&&key.startsWith(prefix))keys.push(key)}return keys}catch(error){storageAccessError=error;return[]}
}
function validWorldEpoch(value){return typeof value==='string'&&/^[A-Za-z0-9._:-]{8,160}$/.test(value)}
function createWorldEpoch(){const value=globalThis.crypto?.randomUUID?.()||`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${sessionId}`;return`world:${value}`}
function storedWorldEpoch(){const value=safeLocalGet(worldEpochStorageKey);return validWorldEpoch(value)?value:null}
function rememberWorldEpoch(value){return validWorldEpoch(value)&&safeLocalSet(worldEpochStorageKey,value)}
function revisionVersion(value){return{rev:Number.isFinite(value?.rev)&&value.rev>=0?value.rev:0,revAuthor:typeof value?.revAuthor==='string'?value.revAuthor:typeof value?.author==='string'?value.author:''}}
function compareRevisionVersions(left,right){const a=revisionVersion(left),b=revisionVersion(right);return a.rev!==b.rev?a.rev-b.rev:a.revAuthor.localeCompare(b.revAuthor)}
function newerRevisionValue(left,right){return compareRevisionVersions(left,right)>=0?left:right}
function clearRecoveryJournalKeys(){let ok=safeLocalRemove(recoveryJournalKey);for(const key of safeLocalKeys(recoveryJournalPrefix))ok=safeLocalRemove(key)&&ok;return ok}
function deleteRetiredWorldData(){
for(const retired of RETIRED_WORLD_STORES){
const prefix=`bend-field:v${retired.schema}:${retired.generation}`;
for(const suffix of[':compact',':recovery',':journal',':revision',':signal',':world:lease'])safeLocalRemove(prefix+suffix);
for(const key of safeLocalKeys(`${prefix}:journal:`))safeLocalRemove(key);
if(typeof indexedDB!=='undefined')try{indexedDB.deleteDatabase(`${prefix}:world`)}catch(_){}
}
}
function manhattan(a,b){return Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1])}
function isPlainObject(value){if(!value||typeof value!=='object'||Array.isArray(value))return false;const proto=Object.getPrototypeOf(value);return proto===Object.prototype||proto===null}
function timeAttackMultiplier(score){return TIME_ATTACK_TIERS.find(tier=>score>=tier.score)?.multiplier||1.25}
function normalizeTimeAttackRun(raw){
if(!isPlainObject(raw)||!TIME_ATTACK_MINUTES.includes(raw.durationMinutes)||!Number.isFinite(raw.startedAt)||raw.startedAt<=0)return null;
const durationMinutes=raw.durationMinutes,startedAt=raw.startedAt,endsAt=startedAt+durationMinutes*60000,
collected=Number.isSafeInteger(raw.collected)&&raw.collected>0?Math.min(raw.collected,MAX_SCORE):0,
baseCollected=Number.isSafeInteger(raw.baseCollected)&&raw.baseCollected>0?Math.min(raw.baseCollected,MAX_SCORE):0;
return{
id:typeof raw.id==='string'&&raw.id?raw.id.slice(0,64):`run-${Math.round(startedAt)}`,
durationMinutes,startedAt,endsAt,
collected,baseCollected,rewardPipelineVersion:2,scoreVersion:SCORE_VERSION,
solves:Number.isSafeInteger(raw.solves)&&raw.solves>0?Math.min(raw.solves,MAX_SOLVES):0
};
}
function normalizeTimeAttackResult(raw){
if(!isPlainObject(raw)||!TIME_ATTACK_MINUTES.includes(raw.durationMinutes))return null;
const collected=Number.isSafeInteger(raw.collected)&&raw.collected>0?Math.min(raw.collected,MAX_SCORE):0,
solves=Number.isSafeInteger(raw.solves)&&raw.solves>0?Math.min(raw.solves,MAX_SOLVES):0,
baseCollected=Number.isSafeInteger(raw.baseCollected)&&raw.baseCollected>0?Math.min(raw.baseCollected,MAX_SCORE):0,
multiplier=baseCollected?collected/baseCollected:1,total=collected,bonus=0,modifierGain=Math.max(0,collected-baseCollected);
return{
id:typeof raw.id==='string'&&raw.id?raw.id.slice(0,64):`result-${Math.round(raw.completedAt||Date.now())}`,
durationMinutes:raw.durationMinutes,collected,baseCollected,rewardPipelineVersion:2,scoreVersion:SCORE_VERSION,solves,multiplier,total,bonus,modifierGain,
completedAt:Number.isFinite(raw.completedAt)&&raw.completedAt>0?raw.completedAt:0
};
}
function normalizeTimeAttackCooldowns(raw){
let sharedEndsAt=0;
if(isPlainObject(raw))for(const minutes of TIME_ATTACK_MINUTES){
const value=Number(raw[minutes]);
if(Number.isFinite(value)&&value>sharedEndsAt)sharedEndsAt=value;
}
return Object.fromEntries(TIME_ATTACK_MINUTES.map(minutes=>[minutes,sharedEndsAt]));
}
function normalizeBonusEvents(raw){
const events={};
if(isPlainObject(raw))for(const[id,value]of Object.entries(raw).slice(0,100000))if(typeof id==='string'&&id.length<=128&&Number.isSafeInteger(value)&&value>0)events[id]=Math.min(value,MAX_SCORE);
return events;
}
function bonusEventTotal(events=data?.bonusEvents){let total=0;for(const value of Object.values(events||{}))total=Math.min(MAX_SCORE,total+value);return total}
function normalizeCloudPending(raw){
const ids=value=>[...new Set((Array.isArray(value)?value:[]).filter(id=>typeof id==='string'&&/^B(?:0|[1-9]\d*)$/.test(id)).slice(0,MAX_BOARDS))];
return{metaIds:ids(raw?.metaIds),stateIds:ids(raw?.stateIds),deleted:ids(raw?.deleted),globalChanged:raw?.globalChanged===true};
}
function normalizePlayerPurchases(raw){
return SharedContracts.normalizePlayerPurchases(raw,{resolveItem:storeItem,maxPaidCost:MAX_SCORE,defaultBuyer:DEFAULT_PLAYER_NAME});
}
function defaultData(){const starterLineColor=randomStarterLineColorId();return{schema:SAVE_SCHEMA,gameplayVersion:GAMEPLAY_DATA_VERSION,worldGeneration:WORLD_GENERATION,worldEpoch:null,globalRev:0,globalRevAuthor:'',metas:{},states:{},quarantine:{},bonusEvents:{},specialMechanicsSeen:[],clockFloor:0,cloudProfile:null,cloudRevision:0,cloudSyncPaused:false,cloudPending:normalizeCloudPending(null),playerName:null,playerPurchases:[],playerEarnedScore:0,starterLineColor,lineColorStyle:starterLineColor,lineEffectStyle:'none',reactionStyle:'classic',lastReaction:'👍',worldFeedRevision:0,nextId:1,solved:0,score:0,bonusScore:0,bonusScoreVersion:SCORE_VERSION,lastSolveAt:0,timeAttack:null,timeAttackRev:0,timeAttackCooldowns:{3:0,5:0,10:0},lastTimeAttack:null,timeAttackSuggestionsDisabled:false,cursorStyle:'default',scoreLensEnabled:false,debugAllItems:false,cameraAnchor:null,selectedBoardId:null,updatedAt:0}}
function validChunkShape(chunks){
if(!Array.isArray(chunks)||chunks.length<1||chunks.length>50)return false;
const set=new Set();
for(const q of chunks){
if(!Array.isArray(q)||q.length!==2||!Number.isInteger(q[0])||!Number.isInteger(q[1])||q[0]<0||q[1]<0||q[0]>64||q[1]>64)return false;
const key=key2(q[0],q[1]);if(set.has(key))return false;set.add(key);
}
if(Math.min(...chunks.map(q=>q[0]))!==0||Math.min(...chunks.map(q=>q[1]))!==0)return false;
const reached=new Set([key2(...chunks[0])]),queue=[chunks[0]];
while(queue.length){const[x,y]=queue.shift();for(const[dx,dy]of[[1,0],[-1,0],[0,1],[0,-1]]){const key=key2(x+dx,y+dy);if(set.has(key)&&!reached.has(key)){reached.add(key);queue.push([x+dx,y+dy])}}}
return reached.size===set.size;
}
function normalizePath(raw){
if(!isPlainObject(raw)||!Number.isInteger(raw.startGate)||!Array.isArray(raw.cells)||raw.cells.length<1||raw.cells.length>5000)return null;
if(raw.endGate!=null&&!Number.isInteger(raw.endGate))return null;
const cells=[];
for(const cell of raw.cells){
if(!Array.isArray(cell)||cell.length!==2||!Number.isInteger(cell[0])||!Number.isInteger(cell[1]))return null;
cells.push([cell[0],cell[1]]);
}
const color=value=>Number.isInteger(value)&&value>=0&&value<LINE_COLORS.length?value:null;
const base=color(raw.colorIndex)??0;
const endGate=raw.endGate??null,detachedStart=raw.detachedStart===true&&endGate==null;
return{startGate:raw.startGate,endGate,openGate:Number.isInteger(raw.openGate)?raw.openGate:null,detachedStart,cells,colorIndex:base,startColorIndex:color(raw.startColorIndex)??base,endColorIndex:color(raw.endColorIndex),lineEffect:LINE_EFFECT_IDS.has(raw.lineEffect)?raw.lineEffect:null,ownerId:typeof raw.ownerId==='string'&&/^[a-f0-9]{16,64}$/i.test(raw.ownerId)?raw.ownerId.toLowerCase():null};
}
function normalizeSpecialProgress(raw){
const crossings=[];
for(const key of Array.isArray(raw?.crossings)?raw.crossings:[]){
if(typeof key!=='string'||!/^-?\d+,-?\d+$/.test(key)||crossings.includes(key))continue;
crossings.push(key);
}
return{crossings:crossings.slice(0,64)};
}
function mergeSpecialProgress(a,b){
return normalizeSpecialProgress({crossings:[...(a?.crossings||[]),...(b?.crossings||[])]});
}
function normalizeSpecialCells(raw,validSet,gates=[]){
const result={crossings:[],warps:[],locks:[],internalGates:[]},reserved=new Set();
const readCell=cell=>{
if(!Array.isArray(cell)||cell.length!==2||!Number.isInteger(cell[0])||!Number.isInteger(cell[1]))return null;
const key=ckey(cell[0],cell[1]);
return validSet.has(key)?[cell[0],cell[1]]:null;
};
for(const rawCell of Array.isArray(raw?.crossings)?raw.crossings:[]){
const cell=readCell(rawCell);if(!cell)continue;const key=ckey(...cell);if(reserved.has(key))continue;
reserved.add(key);result.crossings.push(cell);
}
for(const rawPair of Array.isArray(raw?.warps)?raw.warps:[]){
const a=readCell(rawPair?.a??rawPair?.[0]),b=readCell(rawPair?.b??rawPair?.[1]);
if(!a||!b||sameCell(a,b)||manhattan(a,b)===1)continue;
const ak=ckey(...a),bk=ckey(...b);if(reserved.has(ak)||reserved.has(bk))continue;
reserved.add(ak);reserved.add(bk);result.warps.push({a,b});
}
for(const rawLock of Array.isArray(raw?.locks)?raw.locks:[]){
const keyCell=readCell(rawLock?.key),door=readCell(rawLock?.door);
if(!keyCell||!door||sameCell(keyCell,door))continue;
const kk=ckey(...keyCell),dk=ckey(...door);if(reserved.has(kk)||reserved.has(dk))continue;
reserved.add(kk);reserved.add(dk);result.locks.push({key:keyCell,door});
}
const usedGateIndexes=new Set();
for(const rawPair of Array.isArray(raw?.internalGates)?raw.internalGates:[]){
const a=Number.isInteger(rawPair?.a)?rawPair.a:Number.isInteger(rawPair?.[0])?rawPair[0]:-1,b=Number.isInteger(rawPair?.b)?rawPair.b:Number.isInteger(rawPair?.[1])?rawPair[1]:-1;
const ga=gates[a],gb=gates[b];if(a<0||b<0||a===b||!ga||!gb||usedGateIndexes.has(a)||usedGateIndexes.has(b))continue;
const ac=[ga[0],ga[1]],bc=[gb[0],gb[1]],ak=ckey(...ac),bk=ckey(...bc);if(manhattan(ac,bc)!==1||reserved.has(ak)||reserved.has(bk))continue;
const[dr,dc]=SIDE_D[ga[2]]||[],[br,bcDelta]=SIDE_D[gb[2]]||[];if(ac[0]+dr!==bc[0]||ac[1]+dc!==bc[1]||bc[0]+br!==ac[0]||bc[1]+bcDelta!==ac[1])continue;
usedGateIndexes.add(a);usedGateIndexes.add(b);reserved.add(ak);reserved.add(bk);result.internalGates.push({a,b});
}
return result;
}
function normalizeStore(raw,paths){
if(!isPlainObject(raw))return null;
const directCell=Array.isArray(raw.cell)&&raw.cell.length===2&&Number.isInteger(raw.cell[0])&&Number.isInteger(raw.cell[1])?[raw.cell[0],raw.cell[1]]:null,
legacyPath=Number.isInteger(raw.pathIndex)&&raw.pathIndex>=0&&raw.pathIndex<paths.length?paths[raw.pathIndex]:null;
if(!directCell&&!legacyPath?.cells?.length)return null;
const purchases=[],seen=new Set();
for(const purchase of Array.isArray(raw.purchases)?raw.purchases:[]){
const id=typeof purchase==='string'?purchase:purchase?.id;
if(!STORE_ITEM_IDS.has(id)||seen.has(id))continue;const item=storeItem(id);
seen.add(id);purchases.push({id,buyer:typeof purchase?.buyer==='string'&&purchase.buyer.trim()?purchase.buyer.trim().slice(0,32):DEFAULT_PLAYER_NAME,boughtAt:Number.isFinite(purchase?.boughtAt)&&purchase.boughtAt>0?purchase.boughtAt:0,paidCost:Number.isSafeInteger(purchase?.paidCost)&&purchase.paidCost>=0?Math.min(purchase.paidCost,MAX_SCORE):item.cost});
}
const rawBonus=Number.isSafeInteger(raw.bonus)&&raw.bonus>0?Math.min(raw.bonus,MAX_SCORE):0,itemIds=normalizeStoreItemIds(raw.itemIds);
return{owner:typeof raw.owner==='string'&&raw.owner.trim()?raw.owner.trim().slice(0,32):DEFAULT_PLAYER_NAME,pathIndex:legacyPath?raw.pathIndex:-1,cellIndex:legacyPath?Math.max(0,Math.min(legacyPath.cells.length-1,Number.isInteger(raw.cellIndex)?raw.cellIndex:Math.floor(legacyPath.cells.length/2))):-1,cell:directCell,openedAt:Number.isFinite(raw.openedAt)&&raw.openedAt>0?raw.openedAt:0,priceVersion:Number.isInteger(raw.priceVersion)&&raw.priceVersion>0?raw.priceVersion:STORE_PRICE_VERSION,priceCoefficient:Number.isFinite(raw.priceCoefficient)&&raw.priceCoefficient>=.8&&raw.priceCoefficient<=1.2?raw.priceCoefficient:null,bonus:rawBonus,bonusVersion:SCORE_VERSION,itemIds,purchases};
}
function normalizeState(raw){
const state={paths:[],specialProgress:{crossings:[]},solved:false,expanded:false,expansionRetryRound:0,solvedBy:null,solvedById:null,solvedAt:null,scoreAwarded:0,scoreVersion:SCORE_VERSION,rewardIdentity:null,rewardCoefficient:null,store:null,rev:0,revAuthor:''};
if(!isPlainObject(raw))return state;
if(Array.isArray(raw.paths))for(const candidate of raw.paths.slice(0,MAX_PATHS_PER_BOARD)){const path=normalizePath(candidate);if(path)state.paths.push(path)}
state.specialProgress=normalizeSpecialProgress(raw.specialProgress);
state.solved=raw.solved===true;
state.expanded=raw.expanded===true;
state.expansionRetryRound=Number.isInteger(raw.expansionRetryRound)&&raw.expansionRetryRound>=0?Math.min(raw.expansionRetryRound,1000000):0;
if(typeof raw.solvedBy==='string'&&raw.solvedBy.trim())state.solvedBy=raw.solvedBy.trim().slice(0,32);
if(typeof raw.solvedById==='string'&&/^[a-f0-9]{16,64}$/i.test(raw.solvedById))state.solvedById=raw.solvedById.toLowerCase();
if(state.solved&&!state.solvedBy)state.solvedBy=LEGACY_LOCAL_SOLVER;
if(state.solved&&Number.isFinite(raw.solvedAt)&&raw.solvedAt>0)state.solvedAt=raw.solvedAt;
if(state.solved&&Number.isSafeInteger(raw.scoreAwarded)&&raw.scoreAwarded>0){
state.scoreAwarded=Math.min(raw.scoreAwarded,MAX_SCORE);
}
if(state.solved&&Number.isFinite(raw.rewardIdentity)&&raw.rewardIdentity>=0)state.rewardIdentity=raw.rewardIdentity>>>0;
if(state.solved&&Number.isFinite(raw.rewardCoefficient)&&raw.rewardCoefficient>=.8&&raw.rewardCoefficient<=1.2)state.rewardCoefficient=raw.rewardCoefficient;
if(state.solved)state.store=normalizeStore(raw.store,state.paths);
state.rev=Number.isFinite(raw.rev)&&raw.rev>=0?raw.rev:0;state.revAuthor=typeof raw.revAuthor==='string'?raw.revAuthor.slice(0,128):'';
return state;
}
function repairWarpNumberClues(puzzle,sourceNumbers){
const warps=puzzle?.specialCells?.warps||[];if(!warps.length)return{numbers:sourceNumbers,repaired:false,maxTurns:puzzle.maxTurns||0,totalTurns:puzzle.totalTurns||0};
const reserved=new Set();for(const cell of puzzle.specialCells.crossings||[])reserved.add(ckey(...cell));for(const pair of warps){reserved.add(ckey(...pair.a));reserved.add(ckey(...pair.b))}for(const lock of puzzle.specialCells.locks||[]){reserved.add(ckey(...lock.key));reserved.add(ckey(...lock.door))}
const used=new Set(),numbers=[];let maxTurns=0,totalTurns=0;
for(const path of puzzle.solution){
const analysis=AppLogic.analyzePathTurns(path,puzzle.g,warps,true,(puzzle.specialCells?.internalGates||[]).flatMap(pair=>[pair?.a,pair?.b]).filter(Number.isInteger));if(!analysis.count||!analysis.cells.length)return null;
const bendKeys=new Set(analysis.cells.map(cell=>ckey(...cell))),existing=(sourceNumbers||[]).find(number=>path.cells.some(cell=>sameCell(cell,number))&&bendKeys.has(ckey(number[0],number[1]))&&!used.has(ckey(number[0],number[1]))),
pool=analysis.cells.filter(cell=>!reserved.has(ckey(...cell))&&!used.has(ckey(...cell))),fallback=analysis.cells.filter(cell=>!used.has(ckey(...cell))),cell=existing?[existing[0],existing[1]]:(pool[0]||fallback[0]);
if(!cell)return null;used.add(ckey(...cell));numbers.push([cell[0],cell[1],analysis.count]);maxTurns=Math.max(maxTurns,analysis.count);totalTurns+=analysis.count;
}
const repaired=!sameDataValue(numbers,sourceNumbers||[])||maxTurns!==(puzzle.maxTurns||0)||totalTurns!==(puzzle.totalTurns||0);
return{numbers,repaired,maxTurns,totalTurns};
}
function normalizeStoredPuzzle(raw,chunks,targetLevel){
if(!isPlainObject(raw)||!Array.isArray(raw.g)||!Array.isArray(raw.n)||!Array.isArray(raw.valid)||!Array.isArray(raw.solution))return null;
const maxRows=Math.max(...chunks.map(([,y])=>(y+1)*5)),maxCols=Math.max(...chunks.map(([x])=>(x+1)*5)),validSet=new Set(),valid=[];
for(const cell of raw.valid){if(!Array.isArray(cell)||cell.length!==2||!Number.isInteger(cell[0])||!Number.isInteger(cell[1])||cell[0]<0||cell[1]<0||cell[0]>=maxRows||cell[1]>=maxCols)return null;const key=ckey(cell[0],cell[1]);if(validSet.has(key))return null;validSet.add(key);valid.push([cell[0],cell[1]])}
const obstacleSet=new Set(),obstacles=[];
for(const cell of Array.isArray(raw.obstacles)?raw.obstacles:[]){if(!Array.isArray(cell)||cell.length!==2||!Number.isInteger(cell[0])||!Number.isInteger(cell[1])||cell[0]<0||cell[1]<0||cell[0]>=maxRows||cell[1]>=maxCols)return null;const key=ckey(cell[0],cell[1]);if(validSet.has(key)||obstacleSet.has(key))return null;obstacleSet.add(key);obstacles.push([cell[0],cell[1]])}
if(valid.length+obstacles.length!==chunks.length*25)return null;
const gates=[];
for(const gate of raw.g){if(!Array.isArray(gate)||gate.length!==3||!Number.isInteger(gate[0])||!Number.isInteger(gate[1])||!validSet.has(ckey(gate[0],gate[1]))||!['N','S','W','E'].includes(gate[2]))return null;gates.push([gate[0],gate[1],gate[2]])}
if(!gates.length||gates.length%2!==0)return null;
const numbers=[];
for(const number of raw.n){if(!Array.isArray(number)||number.length!==3||!Number.isInteger(number[0])||!Number.isInteger(number[1])||!Number.isInteger(number[2])||number[2]<0||number[2]>512||!validSet.has(ckey(number[0],number[1])))return null;numbers.push([number[0],number[1],number[2]])}
const specialCells=normalizeSpecialCells(raw.specialCells,validSet,gates),warpPairs=new Map();
for(const pair of specialCells.warps){warpPairs.set(ckey(...pair.a),ckey(...pair.b));warpPairs.set(ckey(...pair.b),ckey(...pair.a))}
const solution=[];
for(const path of raw.solution){
const normalized=normalizePath(path);
if(!normalized||normalized.endGate==null||normalized.startGate<0||normalized.endGate<0||normalized.startGate>=gates.length||normalized.endGate>=gates.length||normalized.cells.some(cell=>!validSet.has(ckey(...cell))))return null;
for(let index=1;index<normalized.cells.length;index++){
const previous=normalized.cells[index-1],cell=normalized.cells[index],warp=warpPairs.get(ckey(...previous));
if(manhattan(previous,cell)!==1&&warp!==ckey(...cell))return null;
}
solution.push(normalized);
}
if(!solution.length)return null;
const covered=new Map(),crossingSet=new Set(specialCells.crossings.map(cell=>ckey(...cell)));
for(const path of solution)for(const cell of path.cells){const key=ckey(...cell),count=covered.get(key)||0;if(count&&(!crossingSet.has(key)||count>=2))return null;covered.set(key,count+1)}
if(covered.size!==valid.length)return null;
const bounds=isPlainObject(raw.bounds)&&Number.isInteger(raw.bounds.w)&&Number.isInteger(raw.bounds.h)?{w:raw.bounds.w,h:raw.bounds.h}:{w:maxCols,h:maxRows};
const puzzle={g:gates,n:numbers,valid,obstacles,specialCells,bounds,axis:typeof raw.axis==='string'?raw.axis:'MIX',solution,level:Number.isInteger(raw.level)?raw.level:targetLevel,maxTurns:Number.isFinite(raw.maxTurns)?raw.maxTurns:0,totalTurns:Number.isFinite(raw.totalTurns)?raw.totalTurns:0,style:typeof raw.style==='string'?raw.style:'stored-procedural',regionalTarget:Number.isInteger(raw.regionalTarget)?raw.regionalTarget:targetLevel,
uniqueness:isPlainObject(raw.uniqueness)?deepClone(raw.uniqueness):null,solutionQuality:isPlainObject(raw.solutionQuality)?deepClone(raw.solutionQuality):null,interactionBurden:isPlainObject(raw.interactionBurden)?deepClone(raw.interactionBurden):null,regionalFallback:raw.regionalFallback===true,regionalOutlier:raw.regionalOutlier===true};
for(const cell of specialCells.crossings)if(covered.get(ckey(...cell))!==2||!crossingStateAtCell({paths:solution},puzzle,cell))return null;
const warpClues=repairWarpNumberClues(puzzle,numbers);if(!warpClues)return null;
puzzle.n=warpClues.numbers;puzzle.maxTurns=warpClues.maxTurns;puzzle.totalTurns=warpClues.totalTurns;if(warpClues.repaired)puzzle._warpTurnsRepaired=true;
try{
puzzle.difficulty=solverDifficulty(puzzle,targetLevel);puzzle.level=puzzle.difficulty;puzzle.complexity=isPlainObject(raw.complexity)?deepClone(raw.complexity):null;return puzzle
}catch(_){return null}
}
function puzzleForStorage(puzzle){
return puzzle?{g:puzzle.g,n:puzzle.n,valid:puzzle.valid,obstacles:Array.isArray(puzzle.obstacles)?puzzle.obstacles:[],specialCells:puzzle.specialCells||{crossings:[],warps:[],locks:[],internalGates:[]},bounds:puzzle.bounds,axis:puzzle.axis,solution:puzzle.solution,level:puzzle.level,difficulty:puzzle.difficulty,maxTurns:puzzle.maxTurns,totalTurns:puzzle.totalTurns,style:puzzle.style,complexity:puzzle.complexity,regionalTarget:puzzle.regionalTarget,regionalFallback:puzzle.regionalFallback===true,regionalOutlier:puzzle.regionalOutlier===true,uniqueness:puzzle.uniqueness||null,solutionQuality:puzzle.solutionQuality||null,interactionBurden:puzzle.interactionBurden||null}:null;
}
function normalizeMeta(id,raw){
if(!/^B(?:0|[1-9]\d*)$/.test(id)||!isPlainObject(raw))return null;
const x=raw.x,y=raw.y,chunks=raw.chunks;
if(!Number.isSafeInteger(x)||!Number.isSafeInteger(y)||Math.abs(x)>MAX_WORLD_COORD||Math.abs(y)>MAX_WORLD_COORD||!validChunkShape(chunks))return null;
const target=Number.isInteger(raw.targetLevel)?raw.targetLevel:raw.level;
const level=Number.isInteger(raw.level)?raw.level:target;
if(!Number.isInteger(target)||target<1||target>10||!Number.isInteger(level)||level<1||level>10||!Number.isFinite(raw.seed))return null;
const sealedSides=[...new Set((Array.isArray(raw.sealedSides)?raw.sealedSides:[]).filter(side=>['N','S','W','E'].includes(side)))],puzzle=normalizeStoredPuzzle(raw.puzzle,chunks,target);
if(!puzzle)return null;
if(puzzle._warpTurnsRepaired)delete puzzle._warpTurnsRepaired;
return{id,x,y,chunks:chunks.map(q=>[q[0],q[1]]),level:puzzle.difficulty,targetLevel:target,seed:raw.seed>>>0,axis:puzzle.axis,entrySide:['N','S','W','E'].includes(raw.entrySide)?raw.entrySide:null,sealedSides,puzzle,generatorVersion:Number.isInteger(raw.generatorVersion)?raw.generatorVersion:GENERATOR_VERSION,rev:Number.isFinite(raw.rev)&&raw.rev>=0?raw.rev:0,revAuthor:typeof raw.revAuthor==='string'?raw.revAuthor.slice(0,128):''};
}
function mechanicTypesForPuzzle(puzzle){
const types=[];if(puzzle?.specialCells?.warps?.length)types.push('warp');if(puzzle?.specialCells?.locks?.length)types.push('lock');if(puzzle?.specialCells?.crossings?.length)types.push('crossing');if(puzzle?.specialCells?.internalGates?.length)types.push('internalGate');return types;
}
function normalizeSnapshot(raw,{quiet=false}={}){
const clean=defaultData();
if(!isPlainObject(raw)){if(!quiet)loadNotices.push('\u4fdd\u5b58\u30c7\u30fc\u30bf\u3092\u521d\u671f\u5316\u3057\u307e\u3057\u305f\u3002');return clean}
if(raw.schema!==SAVE_SCHEMA||raw.worldGeneration!==WORLD_GENERATION){
if(!quiet)loadNotices.push(`\u3053\u306e\u4fdd\u5b58\u5f62\u5f0f\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\uff1a${String(raw.schema)}`);
return clean;
}
const rawMetas=isPlainObject(raw.metas)?raw.metas:{};
const metas=[];
const metaEntries=Object.entries(rawMetas);
if(metaEntries.length>MAX_BOARDS&&!quiet)loadNotices.push('\u76e4\u9762\u6570\u304c\u4e0a\u9650\u3092\u8d85\u3048\u305f\u305f\u3081\u3001\u8d85\u904e\u5206\u3092\u9664\u5916\u3057\u307e\u3057\u305f\u3002');
for(const[id,value]of metaEntries.slice(0,MAX_BOARDS)){const meta=normalizeMeta(id,value);if(meta)metas.push(meta);else if(!quiet)loadNotices.push(`\u7834\u640d\u3057\u305f\u76e4\u9762\u3092\u9664\u5916\u3057\u307e\u3057\u305f\uff1a${id}`)}
metas.sort((a,b)=>a.id==='B0'?-1:b.id==='B0'?1:(b.rev-a.rev)||a.id.localeCompare(b.id,undefined,{numeric:true}));
const occupied=new Set();
for(const meta of metas){
if(meta.chunks.some(([dx,dy])=>occupied.has(key2(meta.x+dx,meta.y+dy)))){if(!quiet)loadNotices.push(`\u91cd\u8907\u3057\u305f\u76e4\u9762\u3092\u9664\u5916\u3057\u307e\u3057\u305f\uff1a${meta.id}`);continue}
clean.metas[meta.id]=meta;
for(const[dx,dy]of meta.chunks)occupied.add(key2(meta.x+dx,meta.y+dy));
}
const rawStates=isPlainObject(raw.states)?raw.states:{};
for(const id of Object.keys(clean.metas))clean.states[id]=normalizeState(rawStates[id]);
const maxId=Math.max(0,...Object.keys(clean.metas).map(id=>Number(id.slice(1))||0));
clean.nextId=Math.max(maxId+1,Number.isInteger(raw.nextId)&&raw.nextId>0?raw.nextId:1);
clean.lastSolveAt=Number.isFinite(raw.lastSolveAt)&&raw.lastSolveAt>0?raw.lastSolveAt:0;
clean.bonusEvents=normalizeBonusEvents(raw.bonusEvents);clean.bonusScore=bonusEventTotal(clean.bonusEvents);clean.bonusScoreVersion=SCORE_VERSION;
clean.timeAttack=normalizeTimeAttackRun(raw.timeAttack);
clean.timeAttackRev=Number.isFinite(raw.timeAttackRev)&&raw.timeAttackRev>=0?raw.timeAttackRev:0;
clean.timeAttackCooldowns=normalizeTimeAttackCooldowns(raw.timeAttackCooldowns);
clean.timeAttackSuggestionsDisabled=raw.timeAttackSuggestionsDisabled===true;
clean.cursorStyle=typeof raw.cursorStyle==='string'&&cursorModel.item(raw.cursorStyle)?raw.cursorStyle.slice(0,64):'default';
clean.scoreLensEnabled=raw.scoreLensEnabled===true;
clean.debugAllItems=false;
const seen=new Set(normalizeSpecialMechanics(raw.specialMechanicsSeen));
for(const meta of Object.values(clean.metas))for(const type of mechanicTypesForPuzzle(meta.puzzle))seen.add(type);
clean.specialMechanicsSeen=[...seen].sort();
clean.lastTimeAttack=normalizeTimeAttackResult(raw.lastTimeAttack);
clean.quarantine=isPlainObject(raw.quarantine)?raw.quarantine:{};
clean.gameplayVersion=GAMEPLAY_DATA_VERSION;
clean.clockFloor=Number.isFinite(raw.clockFloor)&&raw.clockFloor>0?raw.clockFloor:0;
clean.cloudProfile=isPlainObject(raw.cloudProfile)&&typeof raw.cloudProfile.playerId==='string'&&typeof raw.cloudProfile.token==='string'?{playerId:raw.cloudProfile.playerId.slice(0,64),token:raw.cloudProfile.token.slice(0,128)}:null;
clean.cloudRevision=Number.isSafeInteger(raw.cloudRevision)&&raw.cloudRevision>=0?raw.cloudRevision:0;
clean.cloudSyncPaused=raw.cloudSyncPaused===true;
clean.playerName=typeof raw.playerName==='string'&&raw.playerName.trim()?raw.playerName.trim().slice(0,24):null;
clean.playerPurchases=normalizePlayerPurchases(raw.playerPurchases);clean.playerEarnedScore=Number.isSafeInteger(raw.playerEarnedScore)&&raw.playerEarnedScore>=0?raw.playerEarnedScore:0;
clean.starterLineColor=validStarterLineColorId(raw.starterLineColor)?raw.starterLineColor:clean.cloudProfile?starterLineColorForPlayer(clean.cloudProfile.playerId):clean.starterLineColor;
clean.lineColorStyle=migratedLineColorStyle(raw)||clean.starterLineColor;
clean.lineEffectStyle='none';
clean.reactionStyle=REACTION_STYLE_IDS.has(raw.reactionStyle)?raw.reactionStyle:'classic';
clean.lastReaction=raw.lastReaction==='👉🏻'?'🤩':REACTION_EMOJIS.includes(raw.lastReaction)?raw.lastReaction:'👍';
clean.worldFeedRevision=Number.isSafeInteger(raw.worldFeedRevision)&&raw.worldFeedRevision>=0?raw.worldFeedRevision:0;
clean.cloudPending=normalizeCloudPending(raw.cloudPending);
clean.worldEpoch=validWorldEpoch(raw.worldEpoch)?raw.worldEpoch:null;
clean.globalRev=Number.isFinite(raw.globalRev)&&raw.globalRev>=0?raw.globalRev:0;
clean.globalRevAuthor=typeof raw.globalRevAuthor==='string'?raw.globalRevAuthor.slice(0,128):'';
clean.cameraAnchor=isPlainObject(raw.cameraAnchor)&&Number.isFinite(raw.cameraAnchor.centerX)&&Number.isFinite(raw.cameraAnchor.centerY)&&Number.isFinite(raw.cameraAnchor.scale)?{centerX:raw.cameraAnchor.centerX,centerY:raw.cameraAnchor.centerY,scale:Math.max(MIN_CAMERA_SCALE,Math.min(1.8,raw.cameraAnchor.scale))}:null;
clean.selectedBoardId=typeof raw.selectedBoardId==='string'&&/^B(?:0|[1-9]\d*)$/.test(raw.selectedBoardId)?raw.selectedBoardId:null;
clean.updatedAt=Number.isFinite(raw.updatedAt)&&raw.updatedAt>0?raw.updatedAt:0;
return clean;
}
function preserveRecovery(rawText,reason){
if(!rawText)return false;
const envelope={savedAt:new Date().toISOString(),reason,raw:rawText};volatileRecovery=envelope;void persistRecoveryEnvelope(envelope);
const json=JSON.stringify(envelope);
if(safeLocalSet(recoveryStorageKey,json))return true;
try{sessionStorage.setItem(recoveryStorageKey,json);return true}catch(_){return false}
}
async function preserveRecoveryDurably(rawText,reason){
if(!rawText)throw new Error('Recovery snapshot is empty');
const envelope={savedAt:new Date().toISOString(),reason,raw:rawText},json=JSON.stringify(envelope);volatileRecovery=envelope;
let browserCopy=safeLocalSet(recoveryStorageKey,json);if(!browserCopy)try{sessionStorage.setItem(recoveryStorageKey,json);browserCopy=true}catch(_){}
const databaseCopy=await persistRecoveryEnvelope(envelope);if(!browserCopy&&!databaseCopy)throw new Error('\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u4fdd\u5b58\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002');
return{browserCopy,databaseCopy,envelope};
}
function mirrorChunkKey(generation,index){return`${mirrorChunkPrefix}${generation}:${index}`}
function parseMirrorManifest(raw){
if(!raw||raw.charCodeAt(0)!==123)return null;
try{const value=JSON.parse(raw);return value?.format===MIRROR_CHUNK_FORMAT&&typeof value.generation==='string'&&Number.isSafeInteger(value.chunks)&&value.chunks>=0?value:null}catch(_){return null}
}
function readCompactMirrorRaw(){
const manifestRaw=safeLocalGet(storageKey);if(manifestRaw==null)return null;
const manifest=parseMirrorManifest(manifestRaw);if(!manifest)return manifestRaw;
const maxChunks=Math.ceil(LOCAL_MIRROR_MAX_BYTES/MIRROR_CHUNK_BYTES)+8;
if(manifest.chunks>maxChunks||!Number.isFinite(manifest.bytes)||manifest.bytes<0||manifest.bytes>LOCAL_MIRROR_MAX_BYTES)return null;
const chunks=[];let bytes=0;
for(let index=0;index<manifest.chunks;index++){const chunk=safeLocalGet(mirrorChunkKey(manifest.generation,index));if(chunk==null)return null;chunks.push(chunk);bytes+=chunk.length;if(bytes>LOCAL_MIRROR_MAX_BYTES)return null}
if(bytes!==manifest.bytes)return null;return chunks.join('');
}
function clearCompactMirror(){let ok=safeLocalRemove(storageKey);for(const key of safeLocalKeys(mirrorChunkPrefix))ok=safeLocalRemove(key)&&ok;return ok}
function readInitialData(){
const raw=readCompactMirrorRaw();
if(raw!=null){
if(raw.length>MAX_IMPORT_BYTES){preserveRecovery(raw,'Save exceeded safe import size');loadNotices.push('\u4fdd\u5b58\u30c7\u30fc\u30bf\u304c\u8aad\u307f\u8fbc\u307f\u4e0a\u9650\u3092\u8d85\u3048\u3066\u3044\u307e\u3059\u3002');return defaultData()}
try{
const noticeCount=loadNotices.length,snapshot=normalizeSnapshot(JSON.parse(raw));
if(loadNotices.length>noticeCount)preserveRecovery(raw,'Schema recovery');
return snapshot;
}catch(error){preserveRecovery(raw,error?.message||'JSON parse error');loadNotices.push('\u4fdd\u5b58\u30c7\u30fc\u30bf\u3092\u8aad\u307f\u8fbc\u3081\u306a\u3044\u305f\u3081\u3001\u65b0\u3057\u3044\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u958b\u59cb\u3057\u307e\u3059\u3002');return defaultData()}
}
if(storageAccessError)loadNotices.push('\u30d6\u30e9\u30a6\u30b6\u306e\u4fdd\u5b58\u6a5f\u80fd\u3092\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002');
return defaultData();
}
const indexedDbExpectedAtStartup=typeof indexedDB!=='undefined';
let worldDbPromise=null,idbAvailable=indexedDbExpectedAtStartup,idbHealth=indexedDbExpectedAtStartup?'unverified':'unsupported',worldDbAbandoned=false,startupCloudPending=null,startupCloudOutboxLoaded=false,startupWorldEpoch=null,startupWorldControl=null,activeStorageFormat=FIELD_STORAGE_FORMAT,fieldIndexComplete=true,fieldIndexExpectedCount=0,fieldIndexLoadedCount=0,fieldIndexAfterNumber=-1,fieldIndexScanPromise=null,cloudOutboxReady=!cloudApiEnabled;
function requestValue(request){return new Promise((resolve,reject)=>{request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error||new Error('\u4fdd\u5b58\u30c7\u30fc\u30bf\u306e\u8aad\u307f\u53d6\u308a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002'))})}
function transactionDone(tx){return new Promise((resolve,reject)=>{tx.oncomplete=()=>resolve();tx.onerror=()=>reject(tx.error||new Error('\u4fdd\u5b58\u51e6\u7406\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002'));tx.onabort=()=>reject(tx.error||new Error('\u4fdd\u5b58\u51e6\u7406\u304c\u4e2d\u65ad\u3055\u308c\u307e\u3057\u305f\u3002'))})}
function openWorldDb(){
if(!idbAvailable||worldDbAbandoned)return Promise.reject(new Error('\u30d6\u30e9\u30a6\u30b6\u306e\u4fdd\u5b58\u6a5f\u80fd\u3092\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002'));
if(worldDbPromise)return worldDbPromise;
worldDbPromise=new Promise((resolve,reject)=>{
const request=indexedDB.open(worldDbName,IDB_LAYOUT_VERSION);let blockedTimer=0,abandoned=false;
request.onupgradeneeded=event=>{
const db=request.result;
for(const retired of['metas','states','global','recovery','tombstones','outbox'])if(db.objectStoreNames.contains(retired))db.deleteObjectStore(retired);
const controlStore=db.objectStoreNames.contains('control')?request.transaction.objectStore('control'):db.createObjectStore('control',{keyPath:'key'}),
worldsStore=db.objectStoreNames.contains('worlds')?request.transaction.objectStore('worlds'):db.createObjectStore('worlds',{keyPath:'epoch'});
const ensureEpochStore=(name,keyPath)=>{
const store=db.objectStoreNames.contains(name)?request.transaction.objectStore(name):db.createObjectStore(name,{keyPath});
if(!store.indexNames.contains('epoch'))store.createIndex('epoch','epoch',{unique:false});
return store;
};
const boardIndexStore=ensureEpochStore('boardIndex',['epoch','id']);if(!boardIndexStore.indexNames.contains('epochNumber'))boardIndexStore.createIndex('epochNumber',['epoch','number'],{unique:true});
ensureEpochStore('boardPuzzles',['epoch','id']);ensureEpochStore('boardStates',['epoch','id']);
ensureEpochStore('tombstonesV2',['epoch','id']);ensureEpochStore('recoveryV2',['epoch','key']);ensureEpochStore('outboxV2',['epoch','key']);
if(event.oldVersion<IDB_LAYOUT_VERSION){
const epoch=createWorldEpoch(),fresh=defaultData();fresh.worldEpoch=epoch;
const global=globalForStorage(fresh),now=Date.now();
rememberWorldEpoch(epoch);
controlStore.put({key:'active',activeFormat:FIELD_STORAGE_FORMAT,activeEpoch:epoch,activationId:`fresh:${epoch}`,activationVerified:true,switchedAt:now});
worldsStore.put({epoch,status:'active',schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,global,boardCount:0,solvedCount:0,score:0,bounds:{minX:0,minY:0,maxX:1,maxY:1},approximateBytes:0,createdAt:now,source:{kind:'fresh'}});
}
};
request.onsuccess=()=>{clearTimeout(blockedTimer);const db=request.result;if(abandoned||worldDbAbandoned){db.close();reject(new Error('IndexedDB startup was abandoned'));return}db.onversionchange=()=>db.close();idbHealth='ready';resolve(db)};
request.onerror=()=>{clearTimeout(blockedTimer);reject(request.error||new Error('\u4fdd\u5b58\u30c7\u30fc\u30bf\u3092\u958b\u3051\u307e\u305b\u3093\u3067\u3057\u305f\u3002'))};
request.onblocked=()=>{console.warn('BEND FIELD: IndexedDB upgrade is blocked by another tab');if(!blockedTimer)blockedTimer=setTimeout(()=>{abandoned=true;reject(new Error('IndexedDB upgrade remained blocked by another tab'))},IDB_STARTUP_TIMEOUT)};
}).catch(error=>{idbAvailable=false;idbHealth=worldDbAbandoned?'uncertain':'failed';cloudOutboxReady=!cloudApiEnabled;worldDbPromise=null;throw error});
return worldDbPromise;
}
function cloudPendingFromOutboxRows(rows){
const pending={metaIds:[],stateIds:[],deleted:[],globalChanged:false};
for(const row of rows||[]){if(row?.type==='global')pending.globalChanged=true;else if(row?.id&&row.type==='meta')pending.metaIds.push(row.id);else if(row?.id&&row.type==='state')pending.stateIds.push(row.id);else if(row?.id&&row.type==='deleted')pending.deleted.push(row.id)}
return normalizeCloudPending(pending);
}
function epochKeyRange(epoch){return IDBKeyRange.bound([epoch,''],[epoch,'\uffff'])}
async function readActiveWorldControl(db=null){
const target=db||await openWorldDb(),tx=target.transaction('control','readonly'),done=transactionDone(tx),row=await requestValue(tx.objectStore('control').get('active'));await done;
return row||null;
}
async function rollbackUnverifiedWorld(db,control){
if(!control||control.activationVerified!==false||!validWorldEpoch(control.previousEpoch))return null;
const tx=db.transaction(['control','worlds'],'readwrite'),done=transactionDone(tx),controlStore=tx.objectStore('control'),worldsStore=tx.objectStore('worlds'),
[current,previous]=await Promise.all([requestValue(worldsStore.get(control.activeEpoch)),requestValue(worldsStore.get(control.previousEpoch))]);
if(!previous){try{tx.abort()}catch(_){}await done.catch(()=>{});return null}
if(current){current.status='garbage';worldsStore.put(current)}previous.status='active';worldsStore.put(previous);
const restored={key:'active',activeFormat:FIELD_STORAGE_FORMAT,activeEpoch:previous.epoch,activationId:`rollback:${sessionId}:${Date.now()}`,activationVerified:true,switchedAt:trustedNow()};
controlStore.put(restored);await done;rememberWorldEpoch(previous.epoch);return restored;
}
async function loadV2SnapshotFromDb(db,control){
const epoch=control?.activeEpoch;if(!validWorldEpoch(epoch))throw new Error('The active field epoch is invalid.');
const tx=db.transaction(['worlds','boardIndex','outboxV2','recoveryV2','tombstonesV2'],'readonly'),done=transactionDone(tx),
[world,firstIndexRows,outboxRows,recoveryRows,tombstoneRows]=await Promise.all([
requestValue(tx.objectStore('worlds').get(epoch)),requestValue(tx.objectStore('boardIndex').index('epochNumber').getAll(IDBKeyRange.bound([epoch,0],[epoch,Number.MAX_SAFE_INTEGER]),512)),
requestValue(tx.objectStore('outboxV2').getAll(epochKeyRange(epoch))),requestValue(tx.objectStore('recoveryV2').getAll(epochKeyRange(epoch))),
requestValue(tx.objectStore('tombstonesV2').getAll(epochKeyRange(epoch)))
]);await done;
if(!world||world.status!=='active'&&world.status!=='rollback')throw new Error('The active field record is missing.');
const selectedId=typeof world.global?.selectedBoardId==='string'&&/^B(?:0|[1-9]\d*)$/.test(world.global.selectedBoardId)?world.global.selectedBoardId:null,indexRows=[...firstIndexRows];
if(selectedId&&!firstIndexRows.some(row=>row.id===selectedId)){
const selectedTx=db.transaction('boardIndex','readonly'),selectedDone=transactionDone(selectedTx),selectedIndex=await requestValue(selectedTx.objectStore('boardIndex').get([epoch,selectedId]));await selectedDone;if(selectedIndex)indexRows.push(selectedIndex);
}
const tombstones=new Map(tombstoneRows.map(row=>[row.id,row])),
snapshot=normalizeSnapshot({...(world.global||{}),schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,worldEpoch:epoch,metas:{},states:{}},{quiet:true});
boardIndexSummaries.clear();
for(const index of indexRows){
const tombstone=tombstones.get(index.id);
if(tombstone&&compareRevisionVersions(tombstone,{rev:Math.max(index.metaRev||0,index.stateRev||0),revAuthor:(index.metaRev||0)>=(index.stateRev||0)?index.metaRevAuthor||'':index.stateRevAuthor||''})>=0)continue;
if(!/^B(?:0|[1-9]\d*)$/.test(index.id)||!validChunkShape(index.chunks))throw new Error(`The stored field index is invalid for ${index.id}.`);
snapshot.metas[index.id]={id:index.id,x:index.x,y:index.y,chunks:index.chunks.map(chunk=>[chunk[0],chunk[1]]),level:index.level,targetLevel:index.targetLevel,seed:index.seed,axis:index.axis,entrySide:index.entrySide||null,sealedSides:[...(index.sealedSides||[])],puzzle:null,generatorVersion:GENERATOR_VERSION,rev:index.metaRev||0,revAuthor:index.metaRevAuthor||index.revAuthor||'',_summaryOnly:true};
snapshot.states[index.id]=summaryStateFromIndex(index);
}
let maxId=0,scanAfterNumber=-1;for(const index of indexRows)maxId=Math.max(maxId,index.number||Number(index.id.slice(1))||0);for(const index of firstIndexRows)scanAfterNumber=Math.max(scanAfterNumber,index.number||Number(index.id.slice(1))||0);
snapshot.nextId=Math.max(snapshot.nextId||1,maxId+1);snapshot.solved=world.solvedCount||0;snapshot.score=world.score||0;
fieldIndexLoadedCount=Object.keys(snapshot.metas).length;fieldIndexExpectedCount=Math.max(fieldIndexLoadedCount,world.boardCount||0);fieldIndexAfterNumber=scanAfterNumber;fieldIndexComplete=fieldIndexLoadedCount>=fieldIndexExpectedCount;
startupCloudPending=cloudPendingFromOutboxRows(outboxRows);startupCloudOutboxLoaded=true;startupWorldEpoch=epoch;rememberWorldEpoch(epoch);
const coverage=new Map(),databaseJournals=[];for(const row of recoveryRows){if(typeof row?.key!=='string')continue;if(row.key.startsWith('covered:'))coverage.set(row.key.slice(8),row.value?.seq??-1);else if(row.key.startsWith('journal:')&&row.value)databaseJournals.push({...row.value,_dbKey:row.key})}
return{snapshot,coverage,tombstones,databaseJournals,worldEpoch:epoch};
}
async function readV2IndexPage(epoch,afterNumber,limit=512){
const db=await openWorldDb(),tx=db.transaction('boardIndex','readonly'),done=transactionDone(tx),
range=IDBKeyRange.bound([epoch,Math.max(0,afterNumber+1)],[epoch,Number.MAX_SAFE_INTEGER]),
rows=await requestValue(tx.objectStore('boardIndex').index('epochNumber').getAll(range,limit));await done;return rows;
}
function installV2IndexSummary(index){
if(!index||!/^B(?:0|[1-9]\d*)$/.test(index.id)||!validChunkShape(index.chunks))throw new Error(`The stored field index is invalid for ${index?.id||'?'}.`);
if(data.metas[index.id])return false;
const meta={id:index.id,x:index.x,y:index.y,chunks:index.chunks.map(chunk=>[chunk[0],chunk[1]]),level:index.level,targetLevel:index.targetLevel,seed:index.seed,axis:index.axis,entrySide:index.entrySide||null,sealedSides:[...(index.sealedSides||[])],puzzle:null,generatorVersion:GENERATOR_VERSION,rev:index.metaRev||0,revAuthor:index.metaRevAuthor||index.revAuthor||'',_summaryOnly:true},
state=summaryStateFromIndex(index);
data.metas[index.id]=meta;data.states[index.id]=state;fieldIndexLoadedCount++;normalizedStateObjects.add(state);
for(const[dx,dy]of meta.chunks){const key=key2(meta.x+dx,meta.y+dy),owner=occupancy.get(key);if(owner&&owner!==meta.id)throw new Error(`Stored field indexes overlap: ${owner} and ${meta.id}`);occupancy.set(key,meta.id)}
refreshClosedVoidAroundMeta(meta);worldGeometryRevision++;fieldBoundsCache=null;data.nextId=Math.max(data.nextId||1,(index.number||Number(index.id.slice(1))||0)+1);return true;
}
async function completeV2IndexScan(){
if(fieldIndexComplete||activeStorageFormat!==FIELD_STORAGE_FORMAT)return 0;
if(fieldIndexScanPromise)return fieldIndexScanPromise;
fieldIndexScanPromise=(async()=>{
let added=0,pages=0;
while(!fieldIndexComplete){
const page=await readV2IndexPage(data.worldEpoch,fieldIndexAfterNumber,512);if(!page.length){fieldIndexComplete=fieldIndexLoadedCount>=fieldIndexExpectedCount;if(!fieldIndexComplete)throw new Error('The field index ended before its declared board count.');break}
for(const index of page){fieldIndexAfterNumber=Math.max(fieldIndexAfterNumber,index.number||Number(index.id.slice(1))||0);if(installV2IndexSummary(index))added++}
fieldIndexComplete=fieldIndexLoadedCount>=fieldIndexExpectedCount;pages++;worldCountEl.textContent=String(fieldIndexLoadedCount);
if(fieldIndexComplete||pages%8===0){scheduleMinimap(true);scheduleWorldOverview(true)}
if(!fieldIndexComplete)await new Promise(resolve=>setTimeout(resolve,0));
}
if(fieldIndexComplete){orphanPruneDirty=true;invalidateEconomyCaches();statsDirty=true;pruneAndCount();invalidateWorldPresentation();perfCount('fieldIndexScans')}
return added;
})().finally(()=>{fieldIndexScanPromise=null});
return fieldIndexScanPromise;
}
async function loadSnapshotFromDb(journals=[]){
const db=await openWorldDb(),control=await readActiveWorldControl(db);startupWorldControl=control;
if(control?.activeFormat!==FIELD_STORAGE_FORMAT)throw new Error('この版では旧保存形式を読み込めません。');
activeStorageFormat=FIELD_STORAGE_FORMAT;
try{return await loadV2SnapshotFromDb(db,control)}
catch(error){
const restored=await rollbackUnverifiedWorld(db,control).catch(()=>null);
if(restored){startupWorldControl=restored;loadNotices.push('\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u305f\u305f\u3081\u3001\u524d\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u306b\u623b\u3057\u307e\u3057\u305f\u3002');return loadV2SnapshotFromDb(db,restored)}
throw error;
}
}
async function loadCloudOutboxFromDb(){
if(!idbAvailable||!cloudApiEnabled)return normalizeCloudPending(null);
const epoch=validWorldEpoch(data?.worldEpoch)?data.worldEpoch:startupWorldEpoch;if(!validWorldEpoch(epoch))return normalizeCloudPending(null);
const db=await openWorldDb(),tx=db.transaction('outboxV2','readonly'),done=transactionDone(tx),rows=await requestValue(tx.objectStore('outboxV2').getAll(epochKeyRange(epoch)));await done;
return cloudPendingFromOutboxRows(rows);
}
function withStartupTimeout(promise,ms,label){
let timer=null;
return Promise.race([
Promise.resolve(promise),
new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(label)),ms)})
]).finally(()=>clearTimeout(timer));
}
let recoveryJournalsToCover=[];
const recoveredDeletionTombstones=new Map();
const startupRecoveredMetaIds=new Set(),startupRecoveredStateIds=new Set(),startupRecoveredDeletedIds=new Set();
function setRecoveryJournalsToCover(journals){
recoveryJournalsToCover=[...(journals||[])];recoveredDeletionTombstones.clear();
for(const journal of recoveryJournalsToCover)for(const entry of journal.deleted||[]){const id=entry?.id,tombstone={id,rev:Number.isFinite(entry?.rev)?entry.rev:0,revAuthor:typeof entry?.revAuthor==='string'?entry.revAuthor:journal.sessionId},current=recoveredDeletionTombstones.get(id);if(id&&(!current||compareRevisionVersions(tombstone,current)>0))recoveredDeletionTombstones.set(id,tombstone)}
}
function readRecoveryJournals(){
const keys=safeLocalKeys(recoveryJournalPrefix),journals=[];
for(const key of new Set(keys)){const raw=safeLocalGet(key);if(!raw||raw.length>LOCAL_MIRROR_MAX_BYTES)continue;try{const journal=JSON.parse(raw);if(journal?.schema===SAVE_SCHEMA&&journal?.worldGeneration===WORLD_GENERATION&&Number.isFinite(journal.updatedAt))journals.push({...journal,_storageKey:key})}catch(_){}}
return journals.filter(journal=>typeof journal.sessionId==='string'&&Number.isSafeInteger(journal.seq)).sort((a,b)=>(a.updatedAt||0)-(b.updatedAt||0));
}
function mergeRecoveryJournal(base,journal){
if(!journal||!base)return base;
const global=isPlainObject(journal.global)?deepClone(journal.global):null,newerGlobal=(journal.updatedAt||0)>=(base.updatedAt||0),
merged={...deepClone(base),...(newerGlobal&&global?global:{}),metas:deepClone(base.metas||{}),states:deepClone(base.states||{})};
if(!newerGlobal&&global){
merged.nextId=Math.max(merged.nextId||1,global.nextId||1);merged.clockFloor=Math.max(merged.clockFloor||0,global.clockFloor||0);merged.cloudRevision=Math.max(merged.cloudRevision||0,global.cloudRevision||0);
merged.bonusEvents=deepClone(merged.bonusEvents||{});for(const[id,value]of Object.entries(global.bonusEvents||{}))merged.bonusEvents[id]=Math.max(merged.bonusEvents[id]||0,value||0);
const basePending=normalizeCloudPending(merged.cloudPending),journalPending=normalizeCloudPending(global.cloudPending);merged.cloudPending={metaIds:[...new Set([...basePending.metaIds,...journalPending.metaIds])],stateIds:[...new Set([...basePending.stateIds,...journalPending.stateIds])],deleted:[...new Set([...basePending.deleted,...journalPending.deleted])],globalChanged:basePending.globalChanged||journalPending.globalChanged};
if((global.timeAttackRev||0)>(merged.timeAttackRev||0)){merged.timeAttack=global.timeAttack;merged.timeAttackRev=global.timeAttackRev;merged.timeAttackCooldowns=global.timeAttackCooldowns;merged.lastTimeAttack=global.lastTimeAttack||merged.lastTimeAttack}
}
for(const row of journal.metas||[]){
if(!row?.id)continue;const current=merged.metas[row.id];if(!current||compareRevisionVersions(row,current)>=0)merged.metas[row.id]=deepClone(row);
}
for(const entry of journal.deleted||[]){const id=entry?.id,tombstone={rev:Number.isFinite(entry?.rev)?entry.rev:0,revAuthor:typeof entry?.revAuthor==='string'?entry.revAuthor:journal.sessionId},current=merged.metas[id],state=merged.states[id];if(id&&compareRevisionVersions(tombstone,current)>0&&compareRevisionVersions(tombstone,state)>0){delete merged.metas[id];delete merged.states[id]}}
for(const row of journal.states||[]){
if(!row?.id||!merged.metas[row.id])continue;merged.states[row.id]=mergeBoardStates(merged.states[row.id],normalizeState(row.value));
}
merged.updatedAt=Math.max(base.updatedAt||0,journal.updatedAt||0);return normalizeSnapshot(merged,{quiet:true});
}
function mergeRecoveryJournals(base,journals){let merged=base;for(const journal of journals||[])merged=mergeRecoveryJournal(merged,journal);return merged}
function combineRecoveryJournals(...groups){
const merged=new Map();for(const journal of groups.flat().filter(journal=>journal&&typeof journal.sessionId==='string'&&Number.isSafeInteger(journal.seq))){const key=`${journal.sessionId}:${journal.seq}`,current=merged.get(key);merged.set(key,current?{...current,...journal,_storageKey:current._storageKey||journal._storageKey,_dbKey:current._dbKey||journal._dbKey}:journal)}
return[...merged.values()].sort((a,b)=>(a.updatedAt||0)-(b.updatedAt||0));
}
function mergeV2RecoveryJournals(base,journals){
const merged=base;
for(const journal of journals||[]){
if(journal.global&&(journal.updatedAt||0)>=(merged.updatedAt||0)){
for(const key of['bonusEvents','clockFloor','cloudProfile','cloudRevision','cloudSyncPaused','starterLineColor','lineColorStyle','lineEffectStyle','reactionStyle','nextId','lastSolveAt','timeAttack','timeAttackRev','timeAttackCooldowns','lastTimeAttack','specialMechanicsSeen','updatedAt'])if(Object.prototype.hasOwnProperty.call(journal.global,key))merged[key]=deepClone(journal.global[key]);
}
for(const row of journal.metas||[]){if(!row?.id)continue;const normalized=normalizeMeta(row.id,row),current=merged.metas[row.id];if(normalized&&(!current||compareRevisionVersions(normalized,current)>=0)){merged.metas[row.id]=normalized;startupRecoveredMetaIds.add(row.id)}}
for(const entry of journal.deleted||[]){const id=entry?.id;if(!id)continue;delete merged.metas[id];delete merged.states[id];startupRecoveredDeletedIds.add(id)}
for(const row of journal.states||[]){if(!row?.id||!merged.metas[row.id])continue;const normalized=normalizeState(row.value),current=merged.states[row.id];if(!current||compareRevisionVersions(normalized,current)>=0){merged.states[row.id]=normalized;startupRecoveredStateIds.add(row.id)}}
merged.updatedAt=Math.max(merged.updatedAt||0,journal.updatedAt||0);
}
return merged;
}
async function deleteRecoveryCoverageMarkers(sessionIds){
const ids=[...new Set((sessionIds||[]).filter(Boolean))];if(!idbAvailable||!ids.length)return false;
try{
if(!validWorldEpoch(data?.worldEpoch))return false;
const db=await openWorldDb(),tx=db.transaction('recoveryV2','readwrite'),done=transactionDone(tx),store=tx.objectStore('recoveryV2');
for(const id of ids)store.delete([data.worldEpoch,`covered:${id}`]);await done;return true;
}catch(_){return false}
}
function attachWorldEpoch(snapshot,epoch){
const target=snapshot||defaultData(),resolved=validWorldEpoch(epoch)?epoch:validWorldEpoch(target.worldEpoch)?target.worldEpoch:storedWorldEpoch()||createWorldEpoch();
target.worldEpoch=resolved;rememberWorldEpoch(resolved);return target;
}
function abandonWorldDatabase(){
if(!indexedDbExpectedAtStartup)return;worldDbAbandoned=true;idbAvailable=false;idbHealth='uncertain';cloudOutboxReady=!cloudApiEnabled;cloudAvailable=false;
const pending=worldDbPromise;if(pending)void pending.then(db=>{try{db.close()}catch(_){}},()=>{});worldDbPromise=null;
}
async function readInitialDataAsync(){
const boardCount=snapshot=>Object.keys(snapshot?.metas||{}).length;
// The current IndexedDB format is authoritative. Per-session recovery
// journals cover writes that did not reach the active epoch.
const rawMirror=readInitialData(),allJournals=readRecoveryJournals();
if(!idbAvailable){
const epoch=rawMirror.worldEpoch||storedWorldEpoch()||createWorldEpoch(),journals=allJournals.filter(journal=>!journal.worldEpoch||journal.worldEpoch===epoch),fallbackMirror=attachWorldEpoch(mergeRecoveryJournals(rawMirror,journals),epoch),fallbackBoards=boardCount(fallbackMirror);
setRecoveryJournalsToCover(journals);return fallbackMirror
}
const dbLoad=loadSnapshotFromDb(allJournals);
try{
const bundle=await withStartupTimeout(dbLoad,IDB_STARTUP_TIMEOUT,'IndexedDB startup timed out'),epoch=bundle?.worldEpoch||bundle?.snapshot?.worldEpoch||rawMirror.worldEpoch||storedWorldEpoch()||createWorldEpoch(),
journals=combineRecoveryJournals(allJournals,bundle?.databaseJournals||[]).filter(journal=>!journal.worldEpoch||journal.worldEpoch===epoch),coverage=bundle?.coverage||new Map(),
loaded=bundle&&Object.prototype.hasOwnProperty.call(bundle,'snapshot')?bundle.snapshot:bundle,
uncovered=journals.filter(journal=>!journal.sessionId||!Number.isSafeInteger(journal.seq)||(coverage.get(journal.sessionId)??-1)<journal.seq);
for(const journal of journals)if(!uncovered.includes(journal))safeLocalRemove(journal._storageKey);
setRecoveryJournalsToCover(uncovered);
return attachWorldEpoch(mergeV2RecoveryJournals(loaded||defaultData(),uncovered),epoch);
}catch(error){
const epoch=rawMirror.worldEpoch||storedWorldEpoch()||createWorldEpoch(),journals=allJournals.filter(journal=>!journal.worldEpoch||journal.worldEpoch===epoch),fallbackMirror=attachWorldEpoch(mergeRecoveryJournals(rawMirror,journals),epoch),fallbackBoards=boardCount(fallbackMirror);
if(!fallbackBoards)try{const bundle=await withStartupTimeout(dbLoad,4000,'IndexedDB recovery timed out'),recovered=bundle&&Object.prototype.hasOwnProperty.call(bundle,'snapshot')?bundle.snapshot:bundle,dbJournals=combineRecoveryJournals(journals,bundle?.databaseJournals||[]);if(recovered){setRecoveryJournalsToCover(dbJournals);return attachWorldEpoch(mergeV2RecoveryJournals(recovered,dbJournals),bundle?.worldEpoch||epoch)}}catch(_){}
abandonWorldDatabase();
loadNotices.push(fallbackBoards?'\u76f4\u8fd1\u306e\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002':'\u4fdd\u5b58\u30c7\u30fc\u30bf\u3092\u78ba\u8a8d\u3067\u304d\u306a\u3044\u305f\u3081\u3001\u30ed\u30fc\u30ab\u30eb\u30c7\u30fc\u30bf\u3067\u8d77\u52d5\u3057\u307e\u3059\u3002');
console.warn('BEND FIELD: IndexedDB startup fallback',error);
setRecoveryJournalsToCover(journals);
return fallbackMirror;
}
}
async function persistRecoveryEnvelope(envelope){
if(!idbAvailable||!envelope||!validWorldEpoch(data?.worldEpoch))return false;
try{const db=await openWorldDb(),tx=db.transaction('recoveryV2','readwrite'),done=transactionDone(tx);tx.objectStore('recoveryV2').put({epoch:data.worldEpoch,key:'latest',value:envelope});await done;return true}catch(_){return false}
}
async function readRecoveryEnvelope(){
if(idbAvailable&&validWorldEpoch(data?.worldEpoch))try{const db=await openWorldDb(),tx=db.transaction('recoveryV2','readonly'),done=transactionDone(tx),row=await requestValue(tx.objectStore('recoveryV2').get([data.worldEpoch,'latest']));await done;if(row?.value)return row.value}catch(_){}
const raw=safeLocalGet(recoveryStorageKey);return raw?parseJsonOrRaw(raw):volatileRecovery;
}
async function clearDatabaseWorld(newEpoch=createWorldEpoch()){
if(!validWorldEpoch(newEpoch))throw new Error('Invalid world epoch');if(!idbAvailable){rememberWorldEpoch(newEpoch);return newEpoch}
const db=await openWorldDb();
const expected=await activeWorldExpectation(),fresh=defaultData();fresh.worldEpoch=newEpoch;fresh.globalRev=nextRevision();fresh.globalRevAuthor=sessionId;fresh.updatedAt=trustedNow();
try{
const chunks=[[0,0]],puzzle=normalizeStoredPuzzle(deepClone(STARTER_PUZZLE),chunks,1);if(!puzzle)throw new Error('The origin puzzle could not be created.');
const revision=nextRevision(),meta={id:'B0',x:0,y:0,chunks,level:puzzle.difficulty,targetLevel:1,seed:STARTER_SEED,axis:puzzle.axis,entrySide:null,sealedSides:[],puzzle,generatorVersion:GENERATOR_VERSION,rev:revision,revAuthor:sessionId},state=normalizeState(null);state.rev=nextRevision();state.revAuthor=sessionId;fresh.metas.B0=meta;fresh.states.B0=state;fresh.nextId=1;
await writeStagedBoardBatch(newEpoch,[{meta,state}]);const world={epoch:newEpoch,status:'ready',schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,global:globalForStorage(fresh),boardCount:1,solvedCount:0,score:0,bounds:{minX:0,minY:0,maxX:1,maxY:1},approximateBytes:JSON.stringify(meta).length+JSON.stringify(state).length,createdAt:trustedNow(),source:{kind:'reset'}},
tx=db.transaction('worlds','readwrite'),done=transactionDone(tx);tx.objectStore('worlds').put(world);await done;await validateStagedWorldV2(newEpoch,{expectedBoardCount:1,strict:true});await activateReadyWorldV2({epoch:newEpoch,expected,world},{kind:'reset'});rememberWorldEpoch(newEpoch);return newEpoch;
}catch(error){await deleteV2Epoch(newEpoch).catch(()=>{});throw error}
}
let data=defaultData();
const rendered=new Map();
const boardIndexSummaries=new Map(),hydratedBoardLru=new Map();
let hydratedBoardBytes=0;
let occupancy=new Map(),closedVoidKeys=new Set(),activeBoard=null,hudBoardId=null,toastTimer=null,saveTimer=null,lastRevision=Date.now()*1000;
const dirtyMetaIds=new Set(),dirtyStateIds=new Set(),deletedBoardIds=new Set(),deletedBoardRevisions=new Map(),normalizedStateObjects=new WeakSet();
const deletedBoardAuthors=new Map();
const stateStatSignatures=new Map(),stateEconomySignatures=new Map();
const cloudJournalMetaIds=new Set(),cloudJournalStateIds=new Set(),cloudJournalDeletedIds=new Set(),cloudOutboxDeleteKeys=new Set();
let cloudJournalGlobalChanged=false,cloudJournalChangeSeq=0,cloudApplyingRemote=false,globalDirty=false,globalChangeSeq=0,persistQueue=Promise.resolve(),lifecyclePersistenceSuppressed=false,statsDirty=true,cachedStats={solved:0,score:0,earned:0},inventoryCache=null,spentScoreCache=null;
let minimapFrame=0,minimapDirty=true,minimapLongSegments=0,minimapLastDraw=0,minimapDelayTimer=0,minimapRectCache=null;
let overviewFrame=0,overviewDirty=true,overviewLastDraw=0,overviewDelayTimer=0,overviewAllowInteractionBuild=false,overviewInteractionLastBuild=0;
const minimapBase=document.createElement('canvas'),overviewBase=document.createElement('canvas');
let overviewCache=null;
const OVERVIEW_CACHE_OVERSCAN_PX=192,OVERVIEW_INTERACTION_REBUILD_INTERVAL=180;
const adjacencyCache=new Map();
let lineGraphRevision=0,lineGraphCacheRevision=-1,lineComponentCache=new Map(),lineWidthCache=new Map(),lineMinimapGeometryCache=new WeakMap();
let minimapWorldRevision=0,worldGeometryRevision=0,minimapCache=null,minimapPointerState=null,fieldBoundsCache=null,orphanPruneDirty=true;
const perfSamples=new Map(),perfCounters=Object.create(null),perfGauges=Object.create(null);
function perfNow(){return globalThis.performance?.now?.()||Date.now()}
let perfResetAt=perfNow();
function perfStart(){return perfNow()}
function perfObserve(name,value){
if(!Number.isFinite(value)||value<0)return value;
const samples=perfSamples.get(name)||[];samples.push(value);if(samples.length>240)samples.splice(0,samples.length-240);perfSamples.set(name,samples);return value;
}
function perfEnd(name,started){
return perfObserve(name,Math.max(0,perfNow()-started));
}
function perfCount(name,amount=1){perfCounters[name]=(perfCounters[name]||0)+amount}
function perfGauge(name,value){perfGauges[name]=value}
function perfSnapshot(){
const timings={};
for(const[name,samples]of perfSamples){const sorted=[...samples].sort((a,b)=>a-b),pick=q=>sorted.length?sorted[Math.min(sorted.length-1,Math.floor((sorted.length-1)*q))]:0;timings[name]={count:sorted.length,p50:pick(.5),p95:pick(.95),p99:pick(.99),max:sorted[sorted.length-1]||0}}
const elapsedSeconds=Math.max(.001,(perfNow()-perfResetAt)/1000),rates={};for(const[name,value]of Object.entries(perfCounters))rates[`${name}PerSecond`]=value/elapsedSeconds;
return{timings,counters:{...perfCounters},rates,gauges:{...perfGauges,interactionSchedulerVariant:INTERACTION_SCHEDULER_VARIANT,renderedBoards:rendered.size,staticBoards:0,domNodes:document.getElementsByTagName('*').length},elapsedSeconds,capturedAt:new Date().toISOString()};
}
function resetPerf(){perfSamples.clear();for(const key of Object.keys(perfCounters))delete perfCounters[key];for(const key of Object.keys(perfGauges))delete perfGauges[key];interactionIntervals.splice(0);interactionActiveStartedAt=interactionActive()?perfNow():0;interactionLastFrame=0;interactionBestFrameGap=Infinity;interactionFrameOpportunities=0;interactionDroppedFrames=0;reactionLastMetricFrame=0;perfResetAt=perfNow()}
globalThis.BEND_PERF=Object.freeze({snapshot:perfSnapshot,reset:resetPerf,renderReactionSample:options=>renderReactionSample(options),warmEffectCache:(emoji,style)=>warmReactionGlyphCache(emoji,style),clearEffectCaches:()=>clearReactionEffectCaches()});
function observeSchedulerBattery(){
if(typeof navigator.getBattery!=='function')return;
void navigator.getBattery().then(battery=>{
const sample=()=>{perfGauge('batteryLevel',battery.level);perfGauge('batteryCharging',battery.charging?1:0);if(Number.isFinite(battery.dischargingTime))perfGauge('batteryDischargingTime',battery.dischargingTime)};
sample();battery.addEventListener?.('levelchange',sample);battery.addEventListener?.('chargingchange',sample);battery.addEventListener?.('dischargingtimechange',sample);
}).catch(()=>{});
}
observeSchedulerBattery();
function longTaskOverlappedInteraction(entry){
const start=entry.startTime,end=start+entry.duration;if(interactionActiveStartedAt&&end>=interactionActiveStartedAt)return true;
return interactionIntervals.some(([from,to])=>end>=from&&start<=to);
}
if(typeof PerformanceObserver!=='undefined')try{const observer=new PerformanceObserver(list=>{for(const entry of list.getEntries())if(entry.duration>=50){perfCount('longTasks');if(longTaskOverlappedInteraction(entry)){perfCount('interactionLongTasks');perfGauge('lastInteractionLongTaskMs',entry.duration)}perfGauge('lastLongTaskMs',entry.duration)}});observer.observe({type:'longtask',buffered:true})}catch(_){}
if(typeof PerformanceObserver!=='undefined'&&PerformanceObserver.supportedEntryTypes?.includes?.('long-animation-frame'))try{
const observer=new PerformanceObserver(list=>{for(const entry of list.getEntries())if(longTaskOverlappedInteraction(entry)){const scripts=[...(entry.scripts||[])].sort((a,b)=>(b.duration||0)-(a.duration||0)).slice(0,5);perfGauge('lastInteractionLoafScripts',scripts.map(script=>`${script.sourceFunctionName||script.invoker||'anonymous'}@${script.sourceURL?.split('/').pop()||'?'}:${script.sourceCharPosition||0}:${(script.duration||0).toFixed(1)}`).join(','));perfGauge('lastInteractionLoafMs',entry.duration)}});
observer.observe({type:'long-animation-frame',buffered:true});
}catch(_){}
let fpsWindowStarted=perfNow(),fpsFrameCount=0,fpsLastBucket=-1,fpsLastValue=0,fpsLastFrameAt=0;
function markVisualFrame(timestamp=perfNow()){if(fpsLastFrameAt&&timestamp-fpsLastFrameAt<GLOBAL_FRAME_INTERVAL-1)return;const bucket=Math.round(timestamp*10);if(bucket===fpsLastBucket)return;fpsLastBucket=bucket;fpsFrameCount++;fpsLastFrameAt=timestamp}
function refreshFpsCounter(){
if(!fpsCounter)return;const now=perfNow(),elapsed=now-fpsWindowStarted;if(elapsed<450)return;
const interacting=interactionActive(),idle=!interacting&&fpsFrameCount<=2&&now-fpsLastFrameAt>180;
if(idle){fpsCounter.textContent='FPS 待機';fpsCounter.dataset.fps='idle'}
else{fpsLastValue=Math.max(0,Math.round(fpsFrameCount*1000/elapsed));fpsCounter.textContent=`FPS ${fpsLastValue}`;fpsCounter.dataset.fps=fpsLastValue>=28?'good':fpsLastValue>=20?'ok':'low'}
fpsFrameCount=0;fpsWindowStarted=now;
}
setInterval(refreshFpsCounter,500);
const reducedMotionQuery=globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')||null;
let noiseTick=0,noisePainted=false;
function noiseHash(value){value=Math.imul(value^(value>>>16),0x7feb352d);value=Math.imul(value^(value>>>15),0x846ca68b);return(value^(value>>>16))>>>0}
function paintNoiseBackground(staticFrame=false){
if(!noiseCanvas||document.visibilityState==='hidden')return false;
const width=80,height=64;if(noiseCanvas.width!==width)noiseCanvas.width=width;if(noiseCanvas.height!==height)noiseCanvas.height=height;
const context=noiseCanvas.getContext('2d',{alpha:false}),image=context.createImageData(width,height),pixels=image.data,
phase=staticFrame?0:noiseTick++,seed=(data?.metas?.B0?.seed??STARTER_SEED)>>>0;
for(let y=0;y<height;y++)for(let x=0;x<width;x++){
const index=(y*width+x)*4,grain=noiseHash(seed^Math.imul(x+1,0x9e3779b1)^Math.imul(y+1,0x85ebca6b)^Math.imul(phase+1,0xc2b2ae35)),
slow=noiseHash(seed^Math.imul((x>>2)+1,0x27d4eb2d)^Math.imul((y>>2)+1,0x165667b1)^Math.imul((phase>>1)+1,0x9e3779b9)),
value=((grain>>>25)+(slow>>>26))*.72,glow=((grain>>>29)===0?10:0);
pixels[index]=7+Math.min(18,value*.35);pixels[index+1]=12+Math.min(24,value*.52)+glow*.35;pixels[index+2]=17+Math.min(32,value*.72)+glow;pixels[index+3]=255;
}
context.putImageData(image,0,0);noisePainted=true;perfCount('noiseFrames');return true;
}
function scheduleNoiseBackground(force=false){
if(document.visibilityState==='hidden')return;if(force||!noisePainted)paintNoiseBackground(true);
}
reducedMotionQuery?.addEventListener?.('change',()=>{noisePainted=false;scheduleNoiseBackground(true)});
const interactionCommitAt=Object.create(null),interactionInputAt=Object.create(null);
const interactionIntervals=[];
let interactionActiveStartedAt=0,interactionLastFrame=0,interactionFrames=0,interactionSlowFrames=0,interactionBestFrameGap=Infinity,interactionFrameOpportunities=0,interactionDroppedFrames=0,wheelInteractionTimer=0,interactionSettleFrame=0;
let interactionDrawingBoard=null,interactionPendingClaimBoard=null,interactionStyledBoard=null,interactionLastActiveBoard=null;
function recordInteractionCommit(kind,timestamp=perfNow(),inputAt=timestamp,sampleInputAge=true){
const previous=interactionCommitAt[kind]||0,previousInput=interactionInputAt[kind]||0,gap=previous?timestamp-previous:0,inputGap=previousInput&&Number.isFinite(inputAt)?inputAt-previousInput:0;
if(gap>0&&gap<=120&&inputGap<=25)perfObserve(`${kind}FrameGap`,gap);interactionCommitAt[kind]=timestamp;interactionInputAt[kind]=inputAt;
if(sampleInputAge&&Number.isFinite(inputAt)&&inputAt>0&&inputAt<=timestamp+1000)perfObserve(`${kind}InputAge`,Math.max(0,timestamp-inputAt));
}
function refreshInteractionState(force=null){
const activeCandidate=rendered.get(activeBoard)||null,
drawingBoard=interactionDrawingBoard?.drawing?.pointerId!=null&&interactionDrawingBoard.card?.isConnected?interactionDrawingBoard:activeCandidate?.drawing?.pointerId!=null?activeCandidate:null,
pendingClaimBoard=interactionPendingClaimBoard?.pendingClaimPointer&&interactionPendingClaimBoard.card?.isConnected?interactionPendingClaimBoard:activeCandidate?.pendingClaimPointer?activeCandidate:null;
interactionState.set('camera',pan?.id??(pinch?'pinch':null));
interactionState.set('drawing',drawingBoard?.drawing?.pointerId??null);
interactionState.set('claim',pendingClaimBoard?.pendingClaimPointer?.pointerId??null);
if(force===true)interactionState.set('wheel','wheel');else if(force===false)interactionState.clear('wheel');
const active=interactionActive();
interactionDrawingBoard=drawingBoard;interactionPendingClaimBoard=pendingClaimBoard;
const wasActive=document.body.classList.contains('is-interacting');
document.body.classList.toggle('is-interacting',active);
document.body.classList.toggle('is-drawing',Boolean(drawingBoard));
document.body.classList.toggle('is-claim-pending',Boolean(pendingClaimBoard));
topbar?.classList.toggle('drawing-active',Boolean(drawingBoard));world?.classList.toggle('camera-interacting',Boolean(pan||pinch));
const styledDrawingBoard=drawingBoard&&!usesLightweightDragOverlay(drawingBoard)?drawingBoard:null;
const affectedBoards=new Set([interactionStyledBoard,styledDrawingBoard,interactionLastActiveBoard,activeCandidate,rendered.get(hudBoardId),drawingBoard,pendingClaimBoard]);
for(const board of affectedBoards){
if(!board?.card?.isConnected)continue;
board.card.classList.toggle('input-active',board===styledDrawingBoard);
setBoardHudVisibility(board,boardPlayHudVisible(board));
}
interactionStyledBoard=styledDrawingBoard;interactionLastActiveBoard=activeCandidate;
if(active&&!wasActive)interactionActiveStartedAt=perfNow();
else if(!active&&wasActive&&interactionActiveStartedAt){interactionIntervals.push([interactionActiveStartedAt,perfNow()]);if(interactionIntervals.length>64)interactionIntervals.splice(0,interactionIntervals.length-64);interactionActiveStartedAt=0}
if(!active){
interactionLastFrame=0;interactionFrames=0;interactionSlowFrames=0;interactionBestFrameGap=Infinity;interactionFrameOpportunities=0;interactionDroppedFrames=0;
interactionCommitAt.camera=0;interactionCommitAt.pickupLogical=0;interactionCommitAt.pickupVisual=0;interactionCommitAt.pickupPreview=0;
interactionInputAt.camera=0;interactionInputAt.pickupLogical=0;interactionInputAt.pickupVisual=0;interactionInputAt.pickupPreview=0;
if(zoomDetailsDirty)updateZoomPresentation(true);
}
}
function scheduleInteractionSettlePresentation(){
if(interactionSettleFrame)return;
interactionSettleFrame=requestAnimationFrame(()=>{
interactionSettleFrame=0;
if(interactionActive()){scheduleInteractionSettlePresentation();return}
setPickupScenePresentation(null,false);
updateZoomPresentation(true);scheduleLodPass();scheduleMinimap();scheduleWorldOverview();
});
}
function observeInteractionFrame(timestamp=perfNow(),workDuration=0){
if(interactionLastFrame){
const gap=Math.max(0,timestamp-interactionLastFrame);perfObserve('interactionFrameGap',gap);
if(gap>=4&&gap<=50)interactionBestFrameGap=Math.min(interactionBestFrameGap,gap);
const interval=Number.isFinite(interactionBestFrameGap)?interactionBestFrameGap:16.67,opportunities=Math.max(1,Math.round(gap/interval));
interactionFrameOpportunities+=opportunities;interactionDroppedFrames+=Math.max(0,opportunities-1);perfGauge('interactionDroppedFrameRatio',interactionDroppedFrames/Math.max(1,interactionFrameOpportunities));
}
const observedInterval=Number.isFinite(interactionBestFrameGap)?interactionBestFrameGap:16.67,slowWorkThreshold=Math.max(6,observedInterval*.7);
interactionFrames++;if(workDuration>slowWorkThreshold)interactionSlowFrames++;
if(interactionFrames>=12){perfGauge('interactionSlowWorkRatio',interactionSlowFrames/interactionFrames);interactionFrames=0;interactionSlowFrames=0}
interactionLastFrame=timestamp;
}
function pulseWheelInteraction(){
gestureCoordinator.claim('wheel','wheel',{replace:true});refreshInteractionState(true);observeInteractionFrame();clearTimeout(wheelInteractionTimer);wheelInteractionTimer=setTimeout(()=>{gestureCoordinator.release('wheel','wheel');refreshInteractionState(false);recordCameraAnchor();ensureBoards();repositionActiveBoardHud();scheduleMinimap(true);redrawOnlineLayersAfterCamera()},140);
}
function invalidateLineGraphCaches(boardId=null,pathIndex=null){
lineGraphRevision++;
if(!boardId){lineGraphCacheRevision=lineGraphRevision;lineComponentCache=new Map();lineWidthCache=new Map();return}
const prefix=`${boardId}:`,components=new Set();
if(Number.isInteger(pathIndex)){const component=lineComponentCache.get(linePathKey(boardId,pathIndex));if(component)components.add(component)}
else for(const[key,component]of lineComponentCache)if(key.startsWith(prefix))components.add(component);
const meta=data?.metas?.[boardId],paths=data?.states?.[boardId]?.paths||[],indexes=Number.isInteger(pathIndex)?[pathIndex]:paths.map((_,index)=>index);
if(meta?.puzzle)for(const index of indexes){const path=paths[index];if(!path)continue;for(const gateIndex of[...(path.detachedStart?[]:[path.startGate]),path.endGate]){if(gateIndex==null)continue;const hit=matchingNeighborGate(meta,gateIndex);if(!hit)continue;const neighborPaths=data.states?.[hit.meta.id]?.paths||[],neighborIndex=neighborPaths.findIndex(candidate=>pathUsesGate(candidate,hit.gateIndex)),component=neighborIndex>=0?lineComponentCache.get(linePathKey(hit.meta.id,neighborIndex)):null;if(component)components.add(component)}}
for(const component of components)for(const[meta,index]of component.members||[]){const key=linePathKey(meta.id,index);if(lineComponentCache.get(key)===component)lineComponentCache.delete(key);lineWidthCache.delete(key)}
if(Number.isInteger(pathIndex)){lineComponentCache.delete(linePathKey(boardId,pathIndex));lineWidthCache.delete(linePathKey(boardId,pathIndex))}
else for(const key of[...lineWidthCache.keys()])if(key.startsWith(prefix))lineWidthCache.delete(key);
lineGraphCacheRevision=lineGraphRevision;
}
function currentLineGraphCaches(){
if(lineGraphCacheRevision!==lineGraphRevision){lineGraphCacheRevision=lineGraphRevision;lineComponentCache=new Map();lineWidthCache=new Map()}
return{components:lineComponentCache,widths:lineWidthCache,geometries:lineMinimapGeometryCache};
}
function invalidateMinimapWorld(){minimapWorldRevision++;minimapDirty=true;minimapCache=null;scheduleMinimap()}
function invalidateWorldPresentation(){invalidateMinimapWorld();overviewDirty=true;overviewCache=null}
function svgEl(tag,attrs={}){const e=document.createElementNS('http://www.w3.org/2000/svg',tag);for(const[k,v]of Object.entries(attrs))e.setAttribute(k,v);return e}
let soundContext=null,lastStretchSoundAt=0;
function activeSoundContext(){
const Audio=window.AudioContext||window.webkitAudioContext;if(!Audio)return null;
soundContext=soundContext&&soundContext.state!=='closed'?soundContext:new Audio();if(soundContext.state==='suspended')void soundContext.resume();return soundContext;
}
function soundTone(frequency,duration=.08,{gain=.035,type='sine',slide=1,delay=0}={}){
if(!uiSettings.soundEnabled)return;
try{
const context=activeSoundContext();if(!context)return;const now=context.currentTime+delay,osc=context.createOscillator(),amp=context.createGain();
osc.type=type;osc.frequency.setValueAtTime(Math.max(40,frequency),now);osc.frequency.exponentialRampToValueAtTime(Math.max(40,frequency*slide),now+duration);
const outputGain=Math.min(.42,Math.max(.001,gain*SOUND_GAIN_MULTIPLIER));
amp.gain.setValueAtTime(.0001,now);amp.gain.exponentialRampToValueAtTime(outputGain,now+.006);amp.gain.exponentialRampToValueAtTime(.0001,now+duration);
osc.connect(amp).connect(context.destination);osc.start(now);osc.stop(now+duration+.025);
}catch(_){}
}
function soundNoise(duration=.045,{gain=.012,delay=0,highpass=500}={}){
if(!uiSettings.soundEnabled)return;
try{
const context=activeSoundContext();if(!context)return;const now=context.currentTime+delay,length=Math.max(1,Math.ceil(context.sampleRate*duration)),buffer=context.createBuffer(1,length,context.sampleRate),channel=buffer.getChannelData(0);
for(let index=0;index<length;index++)channel[index]=(Math.random()*2-1)*(1-index/length);
const source=context.createBufferSource(),filter=context.createBiquadFilter(),amp=context.createGain();filter.type='highpass';filter.frequency.value=highpass;
const outputGain=Math.min(.24,Math.max(.001,gain*SOUND_GAIN_MULTIPLIER));amp.gain.setValueAtTime(outputGain,now);amp.gain.exponentialRampToValueAtTime(.0001,now+duration);
source.buffer=buffer;source.connect(filter).connect(amp).connect(context.destination);source.start(now);source.stop(now+duration+.01);
}catch(_){}
}
function playSound(kind){
if(kind==='grab'){soundTone(175,.065,{gain:.035,type:'triangle',slide:1.28});soundTone(285,.055,{gain:.022,type:'sine',slide:.9,delay:.014});soundNoise(.026,{gain:.009,highpass:1200})}
else if(kind==='stretch'){const now=performance.now();if(now-lastStretchSoundAt<45)return;lastStretchSoundAt=now;soundTone(132+(now%37),.045,{gain:.018,type:'triangle',slide:1.11})}
else if(kind==='gate'){soundTone(190,.095,{gain:.043,type:'triangle',slide:1.85});soundTone(410,.09,{gain:.03,type:'sine',slide:1.32,delay:.032});soundTone(650,.075,{gain:.018,type:'square',slide:1.08,delay:.07});soundNoise(.04,{gain:.009,delay:.025,highpass:1500})}
else if(kind==='clear'){[[240,0],[320,.055],[480,.11],[720,.18]].forEach(([frequency,delay],index)=>soundTone(frequency,.16,{gain:.04-index*.004,type:index%2?'sine':'triangle',slide:1.35,delay}));soundNoise(.08,{gain:.012,delay:.15,highpass:2200})}
else if(kind==='buy'){soundTone(310,.075,{gain:.037,type:'square',slide:1.22});soundTone(465,.1,{gain:.033,type:'triangle',slide:1.35,delay:.045});soundTone(760,.08,{gain:.022,type:'sine',slide:.96,delay:.105})}
else if(kind==='reset'){soundTone(265,.1,{gain:.035,type:'triangle',slide:.55});soundTone(150,.13,{gain:.028,type:'sine',slide:.68,delay:.04});soundNoise(.065,{gain:.011,delay:.025,highpass:700})}
else if(kind==='remove'){soundTone(330,.075,{gain:.032,type:'sawtooth',slide:.42});soundTone(145,.09,{gain:.024,type:'triangle',slide:.72,delay:.035});soundNoise(.055,{gain:.014,highpass:900})}
else if(kind==='shop'){soundTone(220,.1,{gain:.031,type:'square',slide:1});soundTone(330,.12,{gain:.027,type:'triangle',slide:1.08,delay:.035});soundTone(660,.13,{gain:.021,type:'sine',slide:1.04,delay:.08})}
else if(kind==='warp'){soundTone(115,.16,{gain:.032,type:'sine',slide:3.4});soundTone(540,.12,{gain:.018,type:'triangle',slide:.58,delay:.035});soundNoise(.07,{gain:.009,delay:.02,highpass:1800})}
else if(kind==='key'){soundTone(520,.065,{gain:.027,type:'square',slide:1.16});soundTone(780,.09,{gain:.024,type:'sine',slide:1.12,delay:.04})}
else if(kind==='error'){soundTone(185,.11,{gain:.03,type:'sawtooth',slide:.86});soundTone(155,.12,{gain:.024,type:'square',slide:.9,delay:.055})}
else{soundTone(235,.05,{gain:.022,type:'triangle',slide:1.1});soundTone(355,.038,{gain:.012,type:'sine',slide:.95,delay:.018})}
}
function toast(message,duration=2200){const e=document.querySelector('#toast');e.textContent=message;e.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.remove('show'),duration)}
function nextRevision(){lastRevision=Math.max(lastRevision+1,Date.now()*1000);return lastRevision}
function seedRevisionClock(snapshot=data){
let maximum=Math.max(lastRevision,snapshot?.globalRev||0,(snapshot?.clockFloor||0)*1000);
for(const meta of Object.values(snapshot?.metas||{}))maximum=Math.max(maximum,meta?.rev||0);
for(const state of Object.values(snapshot?.states||{}))maximum=Math.max(maximum,state?.rev||0);
for(const tombstone of recoveredDeletionTombstones.values())maximum=Math.max(maximum,tombstone?.rev||0);
lastRevision=maximum;return maximum;
}
let statusRetryAction=null;
function showStatus(message,{retry=true,fresh=false,onRetry=null}={}){
statusMessage.textContent=message;statusPanel.hidden=false;statusRetryAction=retry?(typeof onRetry==='function'?onRetry:()=>location.reload()):null;
document.querySelector('#retryBtn').hidden=!retry;
const freshButton=document.querySelector('#freshBtn');if(freshButton)freshButton.hidden=!fresh;
}
function hideStatus(){statusPanel.hidden=true;statusRetryAction=null}
async function runStatusRetry(){
const action=statusRetryAction;if(typeof action!=='function')return;
const button=document.querySelector('#retryBtn');if(button)button.disabled=true;
try{await action()}catch(error){console.warn('LinkField: retry failed',error);showStatus(`再接続できませんでした。 ${error?.message||error}`,{retry:true,fresh:false,onRetry:()=>location.reload()})}
finally{if(button)button.disabled=false}
}
const {hash32,rngFrom,shuffle,macroDifficulty,difficultyFitsRegion,SHAPES,solverDifficulty,H_PORT_PROFILES,V_PORT_PROFILES,horizontalBoundaryKey,verticalBoundaryKey}=BendPuzzle;
let workerSeq=0;
const workerJobs=new Map();
let puzzleWorker=null,workerRestartTimer=null,workerFailureCount=0,workerDisabledUntil=0,workerRetryAt=0;
function fallbackWorkerJob(id,error){
const job=workerJobs.get(id);if(!job)return;
workerJobs.delete(id);clearTimeout(job.timeout);
job.reject(error);
}
function failWorkerJobs(error){for(const id of[...workerJobs.keys()])fallbackWorkerJob(id,error)}
function scheduleWorkerRestart(){
clearTimeout(workerRestartTimer);
const delay=Math.min(30000,1000*2**Math.min(5,workerFailureCount++));
workerRetryAt=Date.now()+delay;workerRestartTimer=setTimeout(()=>{workerRestartTimer=null;workerRetryAt=0;createPuzzleWorker()},delay);
}
function createPuzzleWorker(){
if(puzzleWorker){clearTimeout(workerRestartTimer);workerRestartTimer=null;workerRetryAt=0;return puzzleWorker}
if(workerRestartTimer||Date.now()<workerRetryAt)return null;
if(Date.now()<workerDisabledUntil){puzzleWorker=null;return null}
if(typeof Worker==='undefined'){puzzleWorker=null;workerRetryAt=Date.now()+30000;return null}
clearTimeout(workerRestartTimer);workerRestartTimer=null;
try{
const worker=new Worker(`puzzle-worker.js?v=${APP_VERSION}-${GENERATOR_VERSION}`);
worker.onmessage=({data:message})=>{
if(worker!==puzzleWorker)return;
const job=workerJobs.get(message.id);if(!job)return;
if(message.generatorVersion!==GENERATOR_VERSION){const error=new Error('\u76e4\u9762\u751f\u6210\u6a5f\u80fd\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002');fallbackWorkerJob(message.id,error);restartPuzzleWorker(error);return}
workerJobs.delete(message.id);clearTimeout(job.timeout);workerFailureCount=0;
const deliver=()=>message.error?job.reject(new Error(message.error)):job.resolve(message.result??message.puzzle);
if(interactionActive('worker')){perfCount('workerResultsDeferredDuringInteraction');waitForInteractionSettle(null,'worker').then(deliver)}
else deliver();
};
const failed=event=>{if(worker===puzzleWorker)restartPuzzleWorker(new Error(event?.message||'\u76e4\u9762\u751f\u6210\u51e6\u7406\u304c\u505c\u6b62\u3057\u307e\u3057\u305f\u3002'))};
worker.onerror=failed;worker.onmessageerror=failed;
puzzleWorker=worker;return worker;
}catch(error){puzzleWorker=null;console.warn('BEND FIELD: worker unavailable',error);scheduleWorkerRestart();return null}
}
function restartPuzzleWorker(error,{cooldown=false}={}){
if(error&&!error.code)error.code='WORKER_UNAVAILABLE';
const old=puzzleWorker;puzzleWorker=null;if(old)old.terminate();
if(cooldown)workerDisabledUntil=Date.now()+5000;
failWorkerJobs(error);
if(cooldown){clearTimeout(workerRestartTimer);workerRetryAt=Date.now()+5000;workerRestartTimer=setTimeout(()=>{workerRestartTimer=null;workerRetryAt=0;workerDisabledUntil=0;createPuzzleWorker()},5000)}else scheduleWorkerRestart();
}
function generatePuzzleAsync(chunks,seed,level,originX=0,originY=0,timeoutMs=1500,generationOptions=null){
const args={chunks,seed,level,originX,originY,generationOptions};
if(!puzzleWorker)createPuzzleWorker();
if(!puzzleWorker){const error=new Error('\u76e4\u9762\u751f\u6210\u6a5f\u80fd\u3092\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002');error.code='WORKER_UNAVAILABLE';perfCount('workerUnavailable');showStatus('\u76e4\u9762\u751f\u6210\u30ef\u30fc\u30ab\u30fc\u3092\u8d77\u52d5\u3067\u304d\u307e\u305b\u3093\u3002\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002',{retry:true,fresh:false});return Promise.reject(error)}
return new Promise((resolve,reject)=>{
const id=++workerSeq,job={resolve,reject,args,timeout:null};
job.timeout=setTimeout(()=>{
const error=new Error('\u76e4\u9762\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002');
fallbackWorkerJob(id,error);restartPuzzleWorker(error,{cooldown:true});
},Math.max(500,timeoutMs));
workerJobs.set(id,job);
try{puzzleWorker.postMessage({id,...args,generatorVersion:GENERATOR_VERSION})}
catch(error){fallbackWorkerJob(id,error);restartPuzzleWorker(error)}
});
}
function verifyPuzzleUniquenessAsync(puzzle,timeoutMs=650){
const worker=createPuzzleWorker();if(!worker){const error=new Error('\u4e00\u610f\u89e3\u691c\u8a3c\u30ef\u30fc\u30ab\u30fc\u3092\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002');error.code='WORKER_UNAVAILABLE';return Promise.reject(error)}
return new Promise((resolve,reject)=>{
const id=++workerSeq,job={resolve,reject,args:null,timeout:null,kind:'verify'};
job.timeout=setTimeout(()=>{const current=workerJobs.get(id);if(!current)return;workerJobs.delete(id);const result={status:'timeout',solutions:0,nodes:0,elapsedMs:timeoutMs};resolve(result)},Math.max(500,timeoutMs));
workerJobs.set(id,job);
try{worker.postMessage({id,action:'verify',puzzle,limits:{maxMs:450,nodeCap:250000},generatorVersion:GENERATOR_VERSION})}
catch(error){fallbackWorkerJob(id,error);restartPuzzleWorker(error)}
});
}
function refreshClosedVoidAroundMeta(meta){
for(const[dx,dy]of meta?.chunks||[]){const unitX=meta.x+dx,unitY=meta.y+dy;for(const[dr,dc]of[[0,0],...Object.values(SIDE_D)]){const x=unitX+dc,y=unitY+dr,key=key2(x,y);if(isClosedVoidUnit(x,y))closedVoidKeys.add(key);else closedVoidKeys.delete(key)}}
}
function addMetaToOccupancy(meta){
if(!meta||!validChunkShape(meta.chunks))return false;
for(const[dx,dy]of meta.chunks)occupancy.set(key2(meta.x+dx,meta.y+dy),meta.id);
refreshClosedVoidAroundMeta(meta);
worldGeometryRevision++;fieldBoundsCache=null;adjacencyCache.clear();invalidateLineGraphCaches();invalidateWorldPresentation();return true;
}
function rebuildOccupancy(){
occupancy=new Map();closedVoidKeys=new Set();worldGeometryRevision++;fieldBoundsCache=null;
for(const meta of Object.values(data.metas))if(meta&&validChunkShape(meta.chunks))for(const[dx,dy]of meta.chunks)occupancy.set(key2(meta.x+dx,meta.y+dy),meta.id);
const candidates=new Set();for(const key of occupancy.keys()){const[unitX,unitY]=key.split(',').map(Number);for(const[dr,dc]of Object.values(SIDE_D))candidates.add(key2(unitX+dc,unitY+dr))}
for(const key of candidates){const[unitX,unitY]=key.split(',').map(Number);if(isClosedVoidUnit(unitX,unitY))closedVoidKeys.add(key)}
adjacencyCache.clear();invalidateLineGraphCaches();invalidateWorldPresentation();
}
function invalidateEconomyCaches(){inventoryCache=null;spentScoreCache=null;statsDirty=true}
function stateStatSignature(state){return`${state?.solved===true?1:0}:${Math.max(0,Number(state?.scoreAwarded)||0)}`}
function stateEconomySignature(state){
const purchases=state?.store?.purchases||[];let spent=0,hash=2166136261;
for(const purchase of purchases){
const item=storeItem(purchase?.id),paid=item?(purchase.paidCost??item.cost):0,boughtAt=purchase?.boughtAt||0,text=`${purchase?.id||''}\0${paid}\0${boughtAt}\0`;
spent+=paid;for(let index=0;index<text.length;index++)hash=Math.imul(hash^text.charCodeAt(index),16777619)>>>0;
}
return{count:purchases.length,spent,hash};
}
function sameEconomySignature(a,b){return a===b||Boolean(a&&b&&a.count===b.count&&a.spent===b.spent&&a.hash===b.hash)}
function economySignatureSpend(signature){return signature?.spent||0}
function rememberStateSignatures(id,state){
const previousStat=stateStatSignatures.get(id),previousEconomy=stateEconomySignatures.get(id),stat=stateStatSignature(state),economy=stateEconomySignature(state),statChanged=previousStat!==stat,economyChanged=!sameEconomySignature(previousEconomy,economy);
stateStatSignatures.set(id,stat);stateEconomySignatures.set(id,economy);
if(statChanged&&!statsDirty){
const[oldSolved,oldAward]=(previousStat||'0:0').split(':').map(Number),[newSolved,newAward]=stat.split(':').map(Number);
const awardDelta=newAward-oldAward,earned=Number.isFinite(cachedStats.earned)?Math.max(0,cachedStats.earned+awardDelta):null,
score=earned==null?Math.max(0,cachedStats.score+awardDelta):Math.max(0,earned-(spentScoreCache||0));
cachedStats={solved:Math.max(0,cachedStats.solved+newSolved-oldSolved),earned,score};
}
if(economyChanged&&previousEconomy!=null&&!personalEconomyMode()){
const spentDelta=economySignatureSpend(economy)-economySignatureSpend(previousEconomy);
if(spentScoreCache!=null)spentScoreCache=Math.max(0,spentScoreCache+spentDelta);
if(!statsDirty)cachedStats.score=Math.max(0,cachedStats.score-spentDelta);
if(inventoryCache){
inventoryCache=inventoryCache.filter(entry=>entry.meta.id!==id);const meta=data.metas[id],store=state?.store;
if(meta&&store)for(const purchase of store.purchases||[])inventoryCache.push({meta,st:state,store,purchase});
inventoryCache.sort((a,b)=>(a.purchase.boughtAt||0)-(b.purchase.boughtAt||0));
}
}
}
function currentCloudPending(){
return{metaIds:[...cloudJournalMetaIds],stateIds:[...cloudJournalStateIds],deleted:[...cloudJournalDeletedIds],globalChanged:cloudJournalGlobalChanged};
}
function restoreCloudPending(pending=data.cloudPending){
const clean=normalizeCloudPending(pending);cloudJournalMetaIds.clear();cloudJournalStateIds.clear();cloudJournalDeletedIds.clear();
for(const id of clean.metaIds)cloudJournalMetaIds.add(id);for(const id of clean.stateIds)if(data?.states?.[id])cloudJournalStateIds.add(id);for(const id of clean.deleted)cloudJournalDeletedIds.add(id);
cloudJournalGlobalChanged=clean.globalChanged;cloudJournalChangeSeq=0;data.cloudPending=currentCloudPending();
}
function noteCloudRow(kind,id){
if(!id||!cloudApiEnabled)return;
if(kind==='deleted'){cloudJournalMetaIds.delete(id);cloudJournalStateIds.delete(id);cloudJournalDeletedIds.add(id);cloudOutboxDeleteKeys.add(`meta:${id}`);cloudOutboxDeleteKeys.add(`state:${id}`)}
else{cloudJournalDeletedIds.delete(id);cloudOutboxDeleteKeys.add(`deleted:${id}`);(kind==='meta'?cloudJournalMetaIds:cloudJournalStateIds).add(id)}
cloudJournalChangeSeq++;
}
function markGlobalDirty(cloudRelevant=true){globalDirty=true;globalChangeSeq++;if(data){data.globalRev=nextRevision();data.globalRevAuthor=sessionId}if(cloudRelevant&&cloudApiEnabled){cloudJournalGlobalChanged=true;cloudJournalChangeSeq++}}
function hasPendingPersistence(){return globalDirty||dirtyMetaIds.size>0||dirtyStateIds.size>0||deletedBoardIds.size>0||cloudOutboxDeleteKeys.size>0}
function markMetaDirty(id){if(id){dirtyMetaIds.add(id);worldGeometryRevision++;fieldBoundsCache=null;if(data.metas[id]){if(!cloudApplyingRemote)data.metas[id].revAuthor=sessionId;deletedBoardIds.delete(id);deletedBoardRevisions.delete(id);deletedBoardAuthors.delete(id)}if(!cloudApplyingRemote)noteCloudRow('meta',id)}markGlobalDirty(!cloudApplyingRemote);invalidateWorldPresentation()}
function markStateDirty(id,pathIndex=null){if(id){dirtyStateIds.add(id);if(data.states[id]&&!cloudApplyingRemote)data.states[id].revAuthor=sessionId;if(!cloudApplyingRemote)noteCloudRow('state',id);rememberStateSignatures(id,data.states[id])}markGlobalDirty(!cloudApplyingRemote);invalidateLineGraphCaches(id,pathIndex);invalidateWorldPresentation()}
function markBoardDeleted(id){if(!id)return;dirtyMetaIds.delete(id);dirtyStateIds.delete(id);deletedBoardIds.add(id);deletedBoardRevisions.set(id,nextRevision());deletedBoardAuthors.set(id,sessionId);worldGeometryRevision++;fieldBoundsCache=null;if(!cloudApplyingRemote)noteCloudRow('deleted',id);stateStatSignatures.delete(id);stateEconomySignatures.delete(id);markGlobalDirty(!cloudApplyingRemote);statsDirty=true;invalidateEconomyCaches();invalidateLineGraphCaches(id);invalidateWorldPresentation()}
function replaceObjectContents(target,source){
for(const key of Object.keys(target))if(!(key in source))delete target[key];
Object.assign(target,source);return target;
}
function ensureMetaState(id,{dirty=true}={}){
if(!data.states[id]||typeof data.states[id]!=='object'){
data.states[id]=normalizeState(null);normalizedStateObjects.add(data.states[id]);if(dirty)markStateDirty(id);return data.states[id];
}
const state=data.states[id];
if(normalizedStateObjects.has(state))return state;
replaceObjectContents(state,normalizeState(state));normalizedStateObjects.add(state);if(dirty)markStateDirty(id);return state;
}
function metaState(id){return data.states[id]&&typeof data.states[id]==='object'?data.states[id]:null}
function storeItem(id){return STORE_ITEM_CATALOG.get(id)||null}
function normalizeStoreItemIds(raw){
if(!Array.isArray(raw)||new Set(raw).size!==raw.length)return null;
const items=raw.map(storeItem);if(!items.every(Boolean))return null;
const cursors=items.filter(item=>item.cursorStyle).slice(0,6),others=items.filter(item=>!item.cursorStyle).slice(0,6);
return cursors.length===6&&others.length===6?[...cursors,...others].map(item=>item.id):null;
}
function seededStoreItemIds(seed){
const cursorPool=shuffle([...CURSOR_ITEMS],rngFrom(hash32((seed>>>0)^0x5f356495))),
otherPool=shuffle(STORE_ITEMS.filter(item=>!item.cursorStyle),rngFrom(hash32((seed>>>0)^0x2c9277b5))),
fixedTools=otherPool.filter(item=>item.scoreLens).slice(0,1),
cosmeticPool=otherPool.filter(item=>!item.scoreLens),
selectedOthers=shuffle([...fixedTools,...cosmeticPool.slice(0,6-fixedTools.length)],rngFrom(hash32((seed>>>0)^0x6d2b79f5)));
return[...cursorPool.slice(0,6),...selectedOthers].map(item=>item.id);
}
function storeInventoryItems(meta,store=null){
if(!meta)return[];const ids=normalizeStoreItemIds(store?.itemIds)||seededStoreItemIds(meta.seed);
return ids.map(storeItem).filter(Boolean);
}
let playerEconomyLoaded=false;
function personalEconomyMode(){return Boolean(cloudApiEnabled&&data.cloudProfile)}
function onlinePlayerEconomy(){return Boolean(personalEconomyMode()&&cloudAvailable&&playerEconomyLoaded)}
function playerPurchaseForStore(boardId,itemId){return normalizePlayerPurchases(data.playerPurchases).find(purchase=>purchase.boardId===boardId&&purchase.itemId===itemId)||null}
function inventoryEntries(itemId=null){
if(!inventoryCache){
inventoryCache=[];
if(personalEconomyMode())for(const source of normalizePlayerPurchases(data.playerPurchases)){const item=storeItem(source.itemId);if(!item)continue;const meta=data.metas[source.boardId]||null,st=meta?metaState(meta.id):null,store=st?.store||null,purchase={...source,id:source.itemId};inventoryCache.push({meta,st,store,purchase,personal:true})}
else for(const meta of Object.values(data.metas)){const st=metaState(meta.id),store=st.store;if(!store)continue;for(const purchase of store.purchases)inventoryCache.push({meta,st,store,purchase,personal:false})}
inventoryCache.sort((a,b)=>(a.purchase.boughtAt||0)-(b.purchase.boughtAt||0));
}
return itemId?inventoryCache.filter(entry=>entry.purchase.id===itemId):[...inventoryCache];
}
function starterColorGrantCount(itemId=null){
const starter=validStarterLineColorId(data.starterLineColor)?data.starterLineColor:null;if(!starter)return 0;
if(itemId&&itemId!==starter)return 0;
return inventoryEntries(starter).length?0:1;
}
function inventoryCount(itemId=null){return inventoryEntries(itemId).length+starterColorGrantCount(itemId)}
function ownsStoreItem(itemId){return inventoryCount(itemId)>0}
function activeScoreLensCount(){return data.scoreLensEnabled===true?inventoryCount('score-lens'):0}
function spentScoreTotal(){
if(spentScoreCache!=null)return spentScoreCache;
let spent=0;if(personalEconomyMode())for(const purchase of normalizePlayerPurchases(data.playerPurchases)){const item=storeItem(purchase.itemId);if(item)spent+=purchase.paidCost??item.cost}
else for(const id of Object.keys(data.metas)){const store=metaState(id).store;if(!store)continue;for(const purchase of store.purchases){const item=storeItem(purchase.id);if(item)spent+=purchase.paidCost??item.cost}}
spentScoreCache=spent;return spent;
}
function pruneAndCount(){
if(orphanPruneDirty){for(const id of Object.keys(data.states))if(!data.metas[id]){delete data.states[id];markBoardDeleted(id)}orphanPruneDirty=false}
if(statsDirty){
let solved=0,earned=personalEconomyMode()?Math.max(0,Number(data.playerEarnedScore)||0):bonusEventTotal();data.bonusScore=personalEconomyMode()?0:earned;
for(const id of Object.keys(data.metas)){const st=metaState(id);if(st.solved)solved++;if(!personalEconomyMode())earned+=st.scoreAwarded}
cachedStats={solved,earned,score:Math.max(0,earned-spentScoreTotal())};statsDirty=false;
}
data.solved=cachedStats.solved;data.score=cachedStats.score;solvedCountEl.textContent=String(data.solved);scoreCountEl.textContent=debugAllItemsEnabled()?'∞':formatScore(data.score);
}
function snapshotForStorage(updatedAt=trustedNow()){
const started=perfStart();
const metas=Object.fromEntries(metaRowsForStorage().map(meta=>[meta.id,meta]));
const states=Object.fromEntries(stateRowsForStorage().map(row=>[row.id,row.value]));
const snapshot={...globalForStorage(data,updatedAt),cloudPending:currentCloudPending(),metas,states};perfGauge('snapshotBoards',Object.keys(metas).length);perfEnd('snapshotForStorage',started);return snapshot;
}
function compactSnapshot(updatedAt=trustedNow()){
pruneAndCount();
return snapshotForStorage(updatedAt);
}
function mergePurchases(a=[],b=[]){const map=new Map();for(const purchase of[...a,...b]){const current=map.get(purchase.id);if(!current||(purchase.boughtAt||0)>(current.boughtAt||0))map.set(purchase.id,deepClone(purchase))}return[...map.values()]}
function mergeBoardStates(current,incoming){
if(!current)return deepClone(incoming);if(!incoming)return current;
if(current.solved||incoming.solved){
const selected=current.solved&&!incoming.solved?current:incoming.solved&&!current.solved?incoming:compareRevisionVersions(incoming,current)>=0?incoming:current,
other=selected===current?incoming:current,chosen=deepClone(selected);
if(chosen.store&&other.store)chosen.store.purchases=mergePurchases(chosen.store.purchases,other.store.purchases);
chosen.specialProgress=mergeSpecialProgress(current.specialProgress,incoming.specialProgress);
const version=newerRevisionValue(current,incoming);chosen.rev=version.rev||0;chosen.revAuthor=version.revAuthor||'';return chosen;
}
const newer=compareRevisionVersions(incoming,current)>=0?incoming:current,older=newer===incoming?current:incoming,result=deepClone(newer),usedGates=new Set(),usedCells=new Set();
for(const path of result.paths||[]){if(!path.detachedStart)usedGates.add(path.startGate);if(path.endGate!=null)usedGates.add(path.endGate);if(path.openGate!=null)usedGates.add(path.openGate);for(const cell of path.cells)usedCells.add(ckey(...cell))}
for(const path of older.paths||[]){
const gates=[...(path.detachedStart?[]:[path.startGate]),...(path.endGate==null?[]:[path.endGate]),...(path.openGate==null?[]:[path.openGate])],conflict=gates.some(g=>usedGates.has(g))||path.cells.some(cell=>usedCells.has(ckey(...cell)));if(conflict)continue;
result.paths.push(deepClone(path));for(const g of gates)usedGates.add(g);for(const cell of path.cells)usedCells.add(ckey(...cell));
}
result.specialProgress=mergeSpecialProgress(current.specialProgress,incoming.specialProgress);
result.rev=newer.rev||0;result.revAuthor=newer.revAuthor||'';return result;
}
function preserveSolvedBoardState(solvedSource,baseState){
const base=solvedSource?._summaryOnly?baseState:solvedSource,preserved=deepClone(base||baseState||solvedSource||normalizeState(null));
preserved.solved=true;preserved.expanded=Boolean(solvedSource?.expanded||preserved.expanded);
for(const key of['solvedBy','solvedById','solvedAt','rewardIdentity','rewardCoefficient'])if(solvedSource?.[key]!=null)preserved[key]=deepClone(solvedSource[key]);
preserved.scoreAwarded=Math.max(Number(preserved.scoreAwarded)||0,Number(solvedSource?.scoreAwarded)||0);preserved.scoreVersion=SCORE_VERSION;
preserved.specialProgress=mergeSpecialProgress(solvedSource?.specialProgress,preserved.specialProgress);
if(solvedSource?.store){if(preserved.store)preserved.store.purchases=mergePurchases(preserved.store.purchases,solvedSource.store.purchases);else preserved.store=deepClone(solvedSource.store)}
delete preserved._summaryOnly;return preserved;
}
function sameMetaGeometry(a,b){return Boolean(a&&b&&a.x===b.x&&a.y===b.y&&a.seed===b.seed&&sameDataValue(a.chunks,b.chunks)&&sameDataValue(a.sealedSides||[],b.sealedSides||[]))}
function resolveMergedOverlaps(){
const metas=Object.values(data.metas).sort((a,b)=>a.id==='B0'?-1:b.id==='B0'?1:(b.rev||0)-(a.rev||0)||a.id.localeCompare(b.id,undefined,{numeric:true}));
const occupied=new Set(),removed=[];
for(const meta of metas){
if(meta.chunks.some(([dx,dy])=>occupied.has(key2(meta.x+dx,meta.y+dy)))){removed.push(meta.id);continue}
for(const[dx,dy]of meta.chunks)occupied.add(key2(meta.x+dx,meta.y+dy));
}
for(const id of removed){delete data.metas[id];delete data.states[id];markBoardDeleted(id);destroyBoard(rendered.get(id))}
return removed;
}
function clearSharedWorldJournalRow(kind,id){
if(kind==='meta'){cloudJournalMetaIds.delete(id);cloudOutboxDeleteKeys.add(`meta:${id}`)}
else if(kind==='state'){cloudJournalStateIds.delete(id);cloudOutboxDeleteKeys.add(`state:${id}`)}
cloudJournalDeletedIds.delete(id);cloudOutboxDeleteKeys.add(`deleted:${id}`);
}
function applyAuthoritativeSharedGlobal(external){
if(!external)return;
if(Number.isSafeInteger(external.nextId)&&external.nextId>0)data.nextId=external.nextId;
if(Number.isSafeInteger(external.solved)&&external.solved>=0)data.solved=external.solved;
if(Number.isFinite(external.lastSolveAt)&&external.lastSolveAt>=0)data.lastSolveAt=external.lastSolveAt;
if(Array.isArray(external.specialMechanicsSeen))data.specialMechanicsSeen=normalizeSpecialMechanics(external.specialMechanicsSeen);
if(isPlainObject(external.quarantine))data.quarantine=deepClone(external.quarantine);
statsDirty=true;
}
function mergeGlobalFields(external){
if(!external)return;
if(Number.isSafeInteger(external.nextId)&&external.nextId>0)data.nextId=Math.max(data.nextId||1,external.nextId);
if(Number.isSafeInteger(external.solved)&&external.solved>=0)data.solved=Math.max(data.solved||0,external.solved);
if(Number.isFinite(external.lastSolveAt)&&external.lastSolveAt>=0)data.lastSolveAt=Math.max(data.lastSolveAt||0,external.lastSolveAt);
if(Array.isArray(external.specialMechanicsSeen))data.specialMechanicsSeen=normalizeSpecialMechanics([...(data.specialMechanicsSeen||[]),...external.specialMechanicsSeen]);
if(isPlainObject(external.quarantine)){
const merged=deepClone(data.quarantine||{});for(const[id,value]of Object.entries(external.quarantine)){const current=merged[id];if(!current||(value?.failedAt||0)>=(current?.failedAt||0))merged[id]=deepClone(value)}data.quarantine=merged;
}
statsDirty=true;
}
function mergeSnapshotIntoData(external,{finalize=true,authoritativeWorld=false}={}){
if(!external||!isPlainObject(external.metas))return[];
const added=[],authoritativeCompatibleStateIds=authoritativeWorld?new Set():null;
for(const[id,incoming]of Object.entries(external.metas)){
lastRevision=Math.max(lastRevision,incoming.rev||0);const current=data.metas[id];
if(authoritativeWorld){
const compatible=!current||sameMetaGeometry(current,incoming);if(compatible)authoritativeCompatibleStateIds.add(id);
clearSharedWorldJournalRow('meta',id);
if(!current){data.metas[id]=incoming;added.push(id)}
else if(compatible)Object.assign(current,incoming);
else{destroyBoard(rendered.get(id));data.metas[id]=incoming;added.push(id)}
continue;
}
if(!current){data.metas[id]=incoming;added.push(id);continue}
if(compareRevisionVersions(incoming,current)>0){
if(sameMetaGeometry(current,incoming))Object.assign(current,incoming);
else{destroyBoard(rendered.get(id));data.metas[id]=incoming;added.push(id)}
}
}
for(const[id,incoming]of Object.entries(external.states||{})){
lastRevision=Math.max(lastRevision,incoming.rev||0);if(!data.metas[id])continue;
const current=data.states[id];let merged;
if(authoritativeWorld){
const compatible=authoritativeCompatibleStateIds.has(id),retainLocalSolve=compatible&&current?.solved===true&&incoming?.solved!==true,
retainClaimedPending=compatible&&current?.solved!==true&&incoming?.solved!==true&&cloudJournalStateIds.has(id)&&typeof boardClaimOwnedByMe==='function'&&boardClaimOwnedByMe(id);
if(retainLocalSolve){noteCloudRow('state',id);merged=deepClone(current)}
else if(retainClaimedPending)merged=deepClone(current);
else{clearSharedWorldJournalRow('state',id);merged=deepClone(incoming)}
}else merged=mergeBoardStates(current,incoming);
if(!current||!sameDataValue(merged,current)){
if(current&&!authoritativeWorld){merged.rev=nextRevision();merged.revAuthor=sessionId}
data.states[id]=merged;normalizedStateObjects.add(merged);if(current)markStateDirty(id);if(merged.solved===true)removeClaim(id,'cleared');
const board=rendered.get(id);if(board){board.drawing=null;board.solvedPathsRendered=false}
if(data.metas[id].puzzle)sanitizeStateForPuzzle(data.metas[id],{quiet:true});
}
}
if(authoritativeWorld)applyAuthoritativeSharedGlobal(external);else mergeGlobalFields(external);
if(finalize)resolveMergedOverlaps()
return added.filter(id=>data.metas[id]);
}
function setSaveStatus(state,message){saveStatusEl.dataset.state=state;saveStatusEl.textContent=message;saveStatusEl.setAttribute('aria-label',`\u4fdd\u5b58\u72b6\u614b\uff1a${message}`)}
function metaForStorage(m){return{id:m.id,x:m.x,y:m.y,chunks:m.chunks,level:m.level,targetLevel:m.targetLevel,seed:m.seed,axis:m.axis,entrySide:m.entrySide||null,sealedSides:[...new Set((m.sealedSides||[]).filter(side=>['N','S','W','E'].includes(side)))],puzzle:puzzleForStorage(m.puzzle),generatorVersion:m.generatorVersion||GENERATOR_VERSION,rev:m.rev||0,revAuthor:m.revAuthor||''}}
function globalForStorage(source=data,updatedAt=trustedNow()){
const anchor=source===data&&typeof currentCameraAnchor==='function'?currentCameraAnchor():source.cameraAnchor||null;
return{
schema:SAVE_SCHEMA,gameplayVersion:GAMEPLAY_DATA_VERSION,worldGeneration:WORLD_GENERATION,worldEpoch:source.worldEpoch,
globalRev:source.globalRev||0,globalRevAuthor:source.globalRevAuthor||'',appVersion:APP_VERSION,generatorVersion:GENERATOR_VERSION,
quarantine:source.quarantine||{},bonusEvents:source.bonusEvents||{},clockFloor:Math.max(source.clockFloor||0,updatedAt),
cloudProfile:source.cloudProfile,cloudRevision:source.cloudRevision||0,cloudSyncPaused:source.cloudSyncPaused===true,playerName:source.playerName||null,playerPurchases:normalizePlayerPurchases(source.playerPurchases),playerEarnedScore:Number.isSafeInteger(source.playerEarnedScore)&&source.playerEarnedScore>=0?source.playerEarnedScore:0,
starterLineColor:validStarterLineColorId(source.starterLineColor)?source.starterLineColor:STARTER_LINE_COLOR_IDS[0],lineColorStyle:migratedLineColorStyle(source)||(validStarterLineColorId(source.starterLineColor)?source.starterLineColor:STARTER_LINE_COLOR_IDS[0]),lineEffectStyle:'none',reactionStyle:REACTION_STYLE_IDS.has(source.reactionStyle)?source.reactionStyle:'classic',
lastReaction:source.lastReaction==='👉🏻'?'🤩':REACTION_EMOJIS.includes(source.lastReaction)?source.lastReaction:'👍',worldFeedRevision:source.worldFeedRevision||0,
nextId:source.nextId,solved:source.solved||0,score:source.score||0,bonusScore:source.bonusScore||0,bonusScoreVersion:SCORE_VERSION,
lastSolveAt:source.lastSolveAt||0,timeAttack:source.timeAttack,timeAttackRev:source.timeAttackRev||0,timeAttackCooldowns:source.timeAttackCooldowns,
lastTimeAttack:source.lastTimeAttack,timeAttackSuggestionsDisabled:source.timeAttackSuggestionsDisabled===true,cursorStyle:source.cursorStyle||'default',
scoreLensEnabled:source.scoreLensEnabled===true,
debugAllItems:false,
specialMechanicsSeen:normalizeSpecialMechanics(source.specialMechanicsSeen),
cameraAnchor:anchor,selectedBoardId:source===data?(activeBoard||source.selectedBoardId||null):source.selectedBoardId||null,updatedAt,sessionId
};
}
function mergeGlobalRecords(current,incoming){
if(!current)return deepClone(incoming);if(!incoming)return deepClone(current);
if(validWorldEpoch(current.worldEpoch)&&validWorldEpoch(incoming.worldEpoch)&&current.worldEpoch!==incoming.worldEpoch){const error=new Error('World epoch changed in another tab');error.code='STALE_WORLD_EPOCH';throw error}
const newer=compareRevisionVersions({rev:incoming.globalRev,revAuthor:incoming.globalRevAuthor},{rev:current.globalRev,revAuthor:current.globalRevAuthor})>=0?incoming:current,
older=newer===incoming?current:incoming,merged=deepClone(newer);
merged.worldEpoch=incoming.worldEpoch||current.worldEpoch||null;merged.nextId=Math.max(current.nextId||1,incoming.nextId||1);
merged.clockFloor=Math.max(current.clockFloor||0,incoming.clockFloor||0);merged.cloudRevision=Math.max(current.cloudRevision||0,incoming.cloudRevision||0);merged.updatedAt=Math.max(current.updatedAt||0,incoming.updatedAt||0);
merged.bonusEvents={};for(const source of[current.bonusEvents||{},incoming.bonusEvents||{}])for(const[id,value]of Object.entries(source))merged.bonusEvents[id]=Math.max(merged.bonusEvents[id]||0,value||0);
merged.bonusScore=bonusEventTotal(merged.bonusEvents);
const personalPurchases=new Map();for(const purchase of[...(current.playerPurchases||[]),...(incoming.playerPurchases||[])]){if(!purchase?.purchaseId)continue;const prior=personalPurchases.get(purchase.purchaseId);if(!prior||(purchase.boughtAt||0)>=(prior.boughtAt||0))personalPurchases.set(purchase.purchaseId,deepClone(purchase))}merged.playerPurchases=[...personalPurchases.values()];merged.playerEarnedScore=Math.max(Number(current.playerEarnedScore)||0,Number(incoming.playerEarnedScore)||0);
merged.quarantine={};for(const source of[current.quarantine||{},incoming.quarantine||{}])for(const[id,value]of Object.entries(source)){const prior=merged.quarantine[id];if(!prior||(value?.failedAt||0)>=(prior?.failedAt||0))merged.quarantine[id]=deepClone(value)}
if((older.lastSolveAt||0)>(merged.lastSolveAt||0))merged.lastSolveAt=older.lastSolveAt;
if((older.timeAttackRev||0)>(merged.timeAttackRev||0)){merged.timeAttack=deepClone(older.timeAttack);merged.timeAttackRev=older.timeAttackRev;merged.timeAttackCooldowns=deepClone(older.timeAttackCooldowns);merged.lastTimeAttack=deepClone(older.lastTimeAttack)}
else if((older.lastTimeAttack?.completedAt||0)>(merged.lastTimeAttack?.completedAt||0))merged.lastTimeAttack=deepClone(older.lastTimeAttack);
merged.specialMechanicsSeen=normalizeSpecialMechanics([...(current.specialMechanicsSeen||[]),...(incoming.specialMechanicsSeen||[])]);
for(const retired of['combo','comboUntil','comboExpiresAt','comboTimer','bestCombo','undo','undoStack','redoStack'])delete merged[retired];
merged.globalRev=newer.globalRev||0;merged.globalRevAuthor=newer.globalRevAuthor||'';return merged;
}
function applyGlobalRecordToData(record){
if(!record)return;
const priorBonuses=deepClone(data.bonusEvents||{});
for(const key of['worldEpoch','globalRev','globalRevAuthor','gameplayVersion','quarantine','bonusEvents','clockFloor','cloudProfile','cloudRevision','cloudSyncPaused','playerName','playerPurchases','playerEarnedScore','starterLineColor','lineColorStyle','lineEffectStyle','reactionStyle','lastReaction','worldFeedRevision','nextId','solved','score','bonusScore','lastSolveAt','timeAttack','timeAttackRev','timeAttackCooldowns','lastTimeAttack','timeAttackSuggestionsDisabled','cursorStyle','scoreLensEnabled','debugAllItems','specialMechanicsSeen','cameraAnchor','selectedBoardId','updatedAt'])if(Object.prototype.hasOwnProperty.call(record,key))data[key]=deepClone(record[key]);
normalizeEquippedCosmeticsInPlace(data);data.debugAllItems=false;data.bonusScore=bonusEventTotal(data.bonusEvents);lastRevision=Math.max(lastRevision,data.globalRev||0,data.clockFloor?data.clockFloor*1000:0);
if(!sameDataValue(priorBonuses,data.bonusEvents||{}))statsDirty=true;
}
function staleWorldEpochError(){const error=new Error('\u5225\u306e\u30bf\u30d6\u3067\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u7f6e\u304d\u63db\u3048\u3089\u308c\u307e\u3057\u305f\u3002');error.code='STALE_WORLD_EPOCH';return error}
function stateForStorage(state){return AppLogic.stateForStorage(state,{scoreVersion:SCORE_VERSION,normalizeState})}
function summarizeBoardV2(meta,state,epoch=data.worldEpoch){
const puzzle=meta?.puzzle,special=puzzle?.specialCells||{},store=state?.store;
let storeCell=Array.isArray(store?.cell)?[store.cell[0],store.cell[1]]:null;
if(!storeCell&&store&&Number.isInteger(store.pathIndex)&&Number.isInteger(store.cellIndex)){const cell=state.paths?.[store.pathIndex]?.cells?.[store.cellIndex];if(cell)storeCell=[cell[0],cell[1]]}
return{
epoch,id:meta.id,number:Number(meta.id.slice(1))||0,x:meta.x,y:meta.y,chunks:meta.chunks.map(chunk=>[chunk[0],chunk[1]]),level:meta.level,targetLevel:meta.targetLevel,
seed:meta.seed,axis:meta.axis,entrySide:meta.entrySide||null,sealedSides:[...(meta.sealedSides||[])],metaRev:meta.rev||0,stateRev:state?.rev||0,metaRevAuthor:meta.revAuthor||'',stateRevAuthor:state?.revAuthor||'',
solved:state?.solved===true,expanded:state?.expanded===true,solvedBy:state?.solvedBy||null,solvedById:state?.solvedById||null,solvedAt:Number.isFinite(state?.solvedAt)?state.solvedAt:null,scoreAwarded:Math.max(0,Number(state?.scoreAwarded)||0),
hasProgress:Boolean(state?.paths?.length||state?.specialProgress?.crossings?.length),
specialFlags:{crossing:Boolean(special.crossings?.length),warp:Boolean(special.warps?.length),lock:Boolean(special.locks?.length)},
shop:store?{cell:storeCell,itemIds:Array.isArray(store.itemIds)?[...store.itemIds]:[],purchases:(store.purchases||[]).map(purchase=>({id:purchase.id,paidCost:purchase.paidCost||0,boughtAt:purchase.boughtAt||0}))}:null
};
}
function puzzleRecordV2(meta,epoch=data.worldEpoch){return{epoch,id:meta.id,metaRev:meta.rev||0,revAuthor:meta.revAuthor||'',generatorVersion:meta.generatorVersion||GENERATOR_VERSION,puzzle:puzzleForStorage(meta.puzzle)}}
function stateRecordV2(id,state,epoch=data.worldEpoch){return{epoch,id,stateRev:state?.rev||0,revAuthor:state?.revAuthor||'',value:stateForStorage(state)}}
function metaFromV2Records(index,puzzle){
if(!index||!puzzle||index.id!==puzzle.id||index.metaRev!==puzzle.metaRev)return null;
return normalizeMeta(index.id,{id:index.id,x:index.x,y:index.y,chunks:index.chunks,level:index.level,targetLevel:index.targetLevel,seed:index.seed,axis:index.axis,entrySide:index.entrySide||null,sealedSides:index.sealedSides||[],puzzle:puzzle.puzzle,generatorVersion:puzzle.generatorVersion||GENERATOR_VERSION,rev:puzzle.metaRev,revAuthor:puzzle.revAuthor||''});
}
function stateFromV2Record(index,state){
if(!index||!state||index.id!==state.id||index.stateRev!==state.stateRev)return null;
return normalizeState(state.value);
}
function summaryStateFromIndex(index){
const shop=index?.shop;
return{
paths:[],specialProgress:{crossings:[]},solved:index?.solved===true,expanded:index?.expanded===true,expansionRetryRound:0,solvedBy:typeof index?.solvedBy==='string'?index.solvedBy:null,solvedById:typeof index?.solvedById==='string'?index.solvedById:null,solvedAt:Number.isFinite(index?.solvedAt)?index.solvedAt:null,
scoreAwarded:Math.max(0,Number(index?.scoreAwarded)||0),scoreVersion:SCORE_VERSION,rewardIdentity:null,rewardCoefficient:null,
store:shop?{owner:'',pathIndex:-1,cellIndex:-1,cell:Array.isArray(shop.cell)?[...shop.cell]:null,itemIds:[...(shop.itemIds||[])],purchases:deepClone(shop.purchases||[]),summaryCell:Array.isArray(shop.cell)?[...shop.cell]:null}:null,
rev:index?.stateRev||0,revAuthor:index?.stateRevAuthor||index?.revAuthor||'',_summaryOnly:true
};
}
function fieldBoundsFromMetas(metas){
if(metas===data?.metas&&fieldBoundsCache?.revision===worldGeometryRevision)return{...fieldBoundsCache.bounds};
let minX=0,minY=0,maxX=1,maxY=1,found=false;
for(const meta of Object.values(metas||{}))for(const[dx,dy]of meta?.chunks||[]){
const x=meta.x+dx,y=meta.y+dy;if(!found){minX=x;minY=y;maxX=x+1;maxY=y+1;found=true}else{minX=Math.min(minX,x);minY=Math.min(minY,y);maxX=Math.max(maxX,x+1);maxY=Math.max(maxY,y+1)}
}
const bounds={minX,minY,maxX,maxY};if(metas===data?.metas)fieldBoundsCache={revision:worldGeometryRevision,bounds};return{...bounds};
}
function metaRowsForStorage(ids=Object.keys(data.metas)){return ids.map(id=>data.metas[id]&&metaForStorage(data.metas[id])).filter(Boolean)}
function stateRowsForStorage(ids=Object.keys(data.states)){return ids.map(id=>data.states[id]&&({id,value:stateForStorage(data.states[id])})).filter(Boolean)}
async function cloudRowsForStorage(metaIds,stateIds){
const metaSet=new Set(metaIds||[]),stateSet=new Set(stateIds||[]),allIds=[...new Set([...metaSet,...stateSet])],metas=[],states=[];
if(!allIds.length)return{metas,states};
if(activeStorageFormat!==FIELD_STORAGE_FORMAT||!idbAvailable)return{metas:metaRowsForStorage([...metaSet]),states:stateRowsForStorage([...stateSet])};
const db=await openWorldDb(),tx=db.transaction(['control','boardIndex','boardPuzzles','boardStates'],'readonly'),done=transactionDone(tx),key=id=>[data.worldEpoch,id],
[control,indexes,puzzles,stateRecords]=await Promise.all([
requestValue(tx.objectStore('control').get('active')),
Promise.all(allIds.map(id=>requestValue(tx.objectStore('boardIndex').get(key(id))))),
Promise.all(allIds.map(id=>requestValue(tx.objectStore('boardPuzzles').get(key(id))))),
Promise.all(allIds.map(id=>requestValue(tx.objectStore('boardStates').get(key(id)))))
]);await done;
if(!control||control.activeEpoch!==data.worldEpoch||control.activeFormat!==FIELD_STORAGE_FORMAT)throw staleWorldEpochError();
for(let position=0;position<allIds.length;position++){
const id=allIds[position],index=indexes[position],puzzle=puzzles[position],state=stateRecords[position];
if(metaSet.has(id)){
const inMemory=data.metas[id];
if(inMemory?.puzzle)metas.push(metaForStorage(inMemory));
else{
if(!index||!puzzle||index.metaRev!==puzzle.metaRev)throw new Error(`Cloud metadata detail is unavailable: ${id}`);
const restored=normalizeMeta(id,{...index,sealedSides:index.sealedSides||[],puzzle:puzzle.puzzle,generatorVersion:puzzle.generatorVersion||GENERATOR_VERSION,rev:puzzle.metaRev,revAuthor:puzzle.revAuthor||index.metaRevAuthor||''});
if(!restored)throw new Error(`Cloud metadata detail is invalid: ${id}`);metas.push(metaForStorage(restored));
}
}
if(stateSet.has(id)){
const inMemory=data.states[id];
if(inMemory&&!inMemory._summaryOnly)states.push({id,value:stateForStorage(inMemory)});
else{if(!index||!state||index.stateRev!==state.stateRev)throw new Error(`Cloud state detail is unavailable: ${id}`);states.push({id,value:stateForStorage(state.value)})}
}
}
return{metas,states};
}
function readStorageRevision(){
try{const value=JSON.parse(safeLocalGet(storageRevisionKey)||'null');return isPlainObject(value)?value:{}}catch(_){return{}}
}
function updateStorageRevision(patch){
const current=readStorageRevision(),next={
databaseUpdatedAt:Math.max(current.databaseUpdatedAt||0,patch.databaseUpdatedAt||0),
mirrorUpdatedAt:Math.max(current.mirrorUpdatedAt||0,patch.mirrorUpdatedAt||0)
};
return safeLocalSet(storageRevisionKey,JSON.stringify(next));
}
let recoveryJournalSeq=-1;
let recoveryWalPromise=Promise.resolve(),recoveryWalFailed=false;
function persistRecoveryJournalToDb(journal){
if(!idbAvailable||!journal)return Promise.resolve(false);
const run=async()=>{
if(!validWorldEpoch(data?.worldEpoch))return false;
const db=await openWorldDb(),tx=db.transaction('recoveryV2','readwrite'),done=transactionDone(tx),key=`journal:${sessionId}`;
tx.objectStore('recoveryV2').put({epoch:data.worldEpoch,key,value:journal});await done;recoveryWalFailed=false;perfCount('recoveryWalWrites');return true;
};
const result=recoveryWalPromise.then(run,run).catch(error=>{recoveryWalFailed=true;console.warn('BEND FIELD: recovery WAL failed',error);if(document.visibilityState==='visible')showStatus('\u7dca\u6025\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3092\u4fdd\u5b58\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002',{retry:true,fresh:false});return false});recoveryWalPromise=result.then(()=>{},()=>{});return result;
}
function writeDirtyRecoveryJournal(){
if(!hasPendingPersistence())return false;
const updatedAt=Math.max(trustedNow(),(data.updatedAt||0)+1),global=globalForStorage(data,updatedAt),journal={
schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,worldEpoch:data.worldEpoch,updatedAt,sessionId,seq:globalChangeSeq,global:{...global,cloudPending:currentCloudPending()},
metas:metaRowsForStorage([...dirtyMetaIds]),states:stateRowsForStorage([...dirtyStateIds]),deleted:[...deletedBoardIds].map(id=>({id,rev:deletedBoardRevisions.get(id)||updatedAt*1000,revAuthor:deletedBoardAuthors.get(id)||sessionId}))
};
try{
void persistRecoveryJournalToDb(journal);
const json=JSON.stringify(journal);if(json.length>LOCAL_MIRROR_MAX_BYTES){recoveryJournalSeq=globalChangeSeq;perfCount('recoveryJournalOversize');return idbAvailable}
if(!safeLocalSet(sessionRecoveryJournalKey,json))return false;recoveryJournalSeq=globalChangeSeq;perfCount('recoveryJournalWrites');return true;
}catch(_){return false}
}
function clearRecoveryJournalIfCovered(committedSeq,coveredJournals=[]){
let cleared=false;const cleanupSessions=[];
if(recoveryJournalSeq>=0&&recoveryJournalSeq<=committedSeq){if(safeLocalRemove(sessionRecoveryJournalKey)){recoveryJournalSeq=-1;cleanupSessions.push(sessionId);cleared=true}}
for(const journal of coveredJournals)if(journal?._storageKey&&safeLocalRemove(journal._storageKey)){if(journal.sessionId)cleanupSessions.push(journal.sessionId);cleared=true}
if(coveredJournals.length){const coveredKeys=new Set(coveredJournals.map(journal=>journal._storageKey));recoveryJournalsToCover=recoveryJournalsToCover.filter(journal=>!coveredKeys.has(journal._storageKey))}
if(cleanupSessions.length)void deleteRecoveryCoverageMarkers(cleanupSessions);
return cleared;
}
let mirrorRetryAt=0,mirrorCleanupHandle=0,mirrorCleanupQueue=[],mirrorBuildSerial=0;
function scheduleMirrorChunkCleanup(keepGeneration=null){
const keepPrefix=keepGeneration?`${mirrorChunkPrefix}${keepGeneration}:`:null;
mirrorCleanupQueue=safeLocalKeys(mirrorChunkPrefix).filter(key=>!keepPrefix||!key.startsWith(keepPrefix));
if(mirrorCleanupHandle||!mirrorCleanupQueue.length)return;
const run=deadline=>{
mirrorCleanupHandle=0;let removed=0;
while(mirrorCleanupQueue.length&&(deadline?.didTimeout?removed<1:typeof deadline?.timeRemaining==='function'?deadline.timeRemaining()>4:removed<1)){safeLocalRemove(mirrorCleanupQueue.shift());removed++}
if(mirrorCleanupQueue.length)mirrorCleanupHandle=typeof requestIdleCallback==='function'?requestIdleCallback(run,{timeout:MIRROR_IDLE_TIMEOUT}):setTimeout(()=>run({didTimeout:true,timeRemaining:()=>0}),0);
};
mirrorCleanupHandle=typeof requestIdleCallback==='function'?requestIdleCallback(run,{timeout:MIRROR_IDLE_TIMEOUT}):setTimeout(()=>run({didTimeout:true,timeRemaining:()=>0}),0);
}
function writeCompactMirror(snapshot=snapshotForStorage(data.updatedAt||trustedNow()),{authoritative=false}={}){
const started=perfStart();
try{
const head=readStorageRevision(),updatedAt=snapshot?.updatedAt||0;
if(Math.max(head.databaseUpdatedAt||0,head.mirrorUpdatedAt||0)>updatedAt){perfCount('mirrorStaleSkips');if(authoritative){mirrorRetryAt=Date.now()+1000;return false}return true}
const json=JSON.stringify(snapshot);
perfGauge('mirrorBytes',json.length);
if(json.length>LOCAL_MIRROR_MAX_BYTES){perfCount('mirrorOversize');mirrorRetryAt=Date.now()+5*60*1000;return false}
const written=safeLocalSet(storageKey,json);if(written){mirrorRetryAt=0;updateStorageRevision({mirrorUpdatedAt:updatedAt});scheduleMirrorChunkCleanup();perfCount('mirrorWrites')}else mirrorRetryAt=Date.now()+60*1000;return written;
}catch(error){mirrorRetryAt=Date.now()+60*1000;console.warn('BEND FIELD: compact mirror failed',error);return false}
finally{perfEnd('writeCompactMirror',started)}
}
function* jsonValueFragments(value){
if(value===null){yield'null';return}
const type=typeof value;
if(type==='string'){yield JSON.stringify(value);return}
if(type==='number'){yield Number.isFinite(value)?String(value):'null';return}
if(type==='boolean'){yield value?'true':'false';return}
if(type!=='object'){yield'null';return}
if(Array.isArray(value)){
yield'[';for(let index=0;index<value.length;index++){if(index)yield',';yield*jsonValueFragments(value[index])}yield']';return;
}
yield'{';let first=true;
for(const key of Object.keys(value)){const entry=value[key];if(entry===undefined||typeof entry==='function'||typeof entry==='symbol')continue;if(!first)yield',';first=false;yield JSON.stringify(key);yield':';yield*jsonValueFragments(entry)}
yield'}';
}
function mirrorBuildGeneration(updatedAt,seq){return`${Math.max(0,updatedAt).toString(36)}-${Math.max(0,seq).toString(36)}-${sessionId.replace(/[^A-Za-z0-9_-]/g,'').slice(-20)}-${(++mirrorBuildSerial).toString(36)}`}
function discardMirrorBuild(build=mirrorBuild){if(build?.generation){const committed=parseMirrorManifest(safeLocalGet(storageKey));scheduleMirrorChunkCleanup(committed?.generation||null)}if(build===mirrorBuild)mirrorBuild=null}
function initializeMirrorSerialization(build){
const snapshot={...globalForStorage(data,build.updatedAt),cloudPending:currentCloudPending(),metas:build.metas,states:build.states};
build.serializer=jsonValueFragments(snapshot);build.pendingChunk='';build.chunkIndex=0;build.bytes=0;build.serializerDone=false;build.generation=mirrorBuildGeneration(build.updatedAt,build.seq);build.cleanupKeys=safeLocalKeys(mirrorChunkPrefix);safeLocalRemove(storageKey);build.stage=build.cleanupKeys.length?'cleanup':'serialize';
}
function appendMirrorFragment(build,fragment){
let offset=0;
while(offset<fragment.length){const capacity=MIRROR_CHUNK_BYTES-build.pendingChunk.length,take=Math.min(capacity,fragment.length-offset);build.pendingChunk+=fragment.slice(offset,offset+take);offset+=take;if(build.pendingChunk.length>=MIRROR_CHUNK_BYTES)return offset}
return offset;
}
function writeMirrorBuildChunk(build){
if(!build.pendingChunk)return true;
const started=perfStart(),key=mirrorChunkKey(build.generation,build.chunkIndex),chunk=build.pendingChunk;
if(!safeLocalSet(key,chunk)){perfEnd('mirrorChunkWrite',started);return false}
build.pendingChunk='';build.chunkIndex++;build.bytes+=chunk.length;perfGauge('mirrorChunkWriteMs',perfNow()-started);perfEnd('mirrorChunkWrite',started);perfCount('mirrorChunkWrites');return true;
}
function commitMirrorBuild(build){
const head=readStorageRevision();
if(Math.max(head.databaseUpdatedAt||0,head.mirrorUpdatedAt||0)>build.updatedAt){perfCount('mirrorStaleSkips');discardMirrorBuild(build);return true}
const manifest={format:MIRROR_CHUNK_FORMAT,generation:build.generation,chunks:build.chunkIndex,bytes:build.bytes,updatedAt:build.updatedAt};
if(!safeLocalSet(storageKey,JSON.stringify(manifest)))return false;
mirrorRetryAt=0;updateStorageRevision({mirrorUpdatedAt:build.updatedAt});perfGauge('mirrorBytes',build.bytes);perfGauge('mirrorChunks',build.chunkIndex);perfCount('mirrorWrites');scheduleMirrorChunkCleanup(build.generation);return true;
}
let mirrorIdleHandle=0,mirrorDelayTimer=0,mirrorCheckpointDirty=false,mirrorBuild=null;
function* storageEntryIterator(source){for(const id in source)if(Object.prototype.hasOwnProperty.call(source,id))yield[id,source[id]]}
function cancelMirrorCheckpoint(clearDirty=false){
if(mirrorDelayTimer){clearTimeout(mirrorDelayTimer);mirrorDelayTimer=0}
if(mirrorIdleHandle){if(typeof cancelIdleCallback==='function')cancelIdleCallback(mirrorIdleHandle);else clearTimeout(mirrorIdleHandle);mirrorIdleHandle=0}
discardMirrorBuild();if(clearDirty)mirrorCheckpointDirty=false;
}
function runMirrorCheckpoint(deadline=null){
mirrorIdleHandle=0;
if(!idbAvailable||!mirrorCheckpointDirty)return;
const interacting=Boolean(pan||pinch||[...rendered.values()].some(board=>board.drawing?.pointerId!=null));
if(interacting||hasPendingPersistence()){discardMirrorBuild();scheduleMirrorCheckpoint();return}
if(Date.now()<mirrorRetryAt){scheduleMirrorCheckpoint(mirrorRetryAt-Date.now());return}
const checkpointUpdatedAt=data.updatedAt||0,checkpointSeq=globalChangeSeq;
if(!checkpointUpdatedAt){scheduleMirrorCheckpoint();return}
if(!mirrorBuild||mirrorBuild.updatedAt!==checkpointUpdatedAt||mirrorBuild.seq!==checkpointSeq){discardMirrorBuild();mirrorBuild={updatedAt:checkpointUpdatedAt,seq:checkpointSeq,metaIterator:storageEntryIterator(data.metas),stateIterator:storageEntryIterator(data.states),metaDone:false,stateDone:false,metas:{},states:{},slices:0,stage:'rows'}}
const build=mirrorBuild,sliceStarted=perfNow(),canContinue=processed=>deadline?.didTimeout?processed<64:typeof deadline?.timeRemaining==='function'?deadline.timeRemaining()>4:processed<64;let processed=0;build.slices++;
if(build.stage==='rows'){
while(!build.metaDone&&canContinue(processed++)){const next=build.metaIterator.next();if(next.done){build.metaDone=true;break}const[id,meta]=next.value;if(meta)build.metas[id]=metaForStorage(meta)}
while(build.metaDone&&!build.stateDone&&canContinue(processed++)){const next=build.stateIterator.next();if(next.done){build.stateDone=true;break}const[id,state]=next.value;if(state)build.states[id]=stateForStorage(state)}
if(build.metaDone&&build.stateDone)initializeMirrorSerialization(build);
}
if(build.stage==='cleanup'){
if(build.cleanupKeys.length){safeLocalRemove(build.cleanupKeys.shift());perfCount('mirrorChunkRemovals');perfGauge('mirrorBuildSliceMs',perfNow()-sliceStarted);scheduleMirrorCheckpoint(0);return}
build.stage='serialize';
}
if(build.stage==='serialize'){
let fragmentOffset=build.fragmentOffset||0,fragment=build.fragment||'';
while(canContinue(processed++)){
if(fragmentOffset<fragment.length){fragmentOffset+=appendMirrorFragment(build,fragment.slice(fragmentOffset));if(build.pendingChunk.length>=MIRROR_CHUNK_BYTES){build.fragment=fragment;build.fragmentOffset=fragmentOffset;if(!writeMirrorBuildChunk(build)){mirrorRetryAt=Date.now()+60*1000;discardMirrorBuild(build);scheduleMirrorCheckpoint(mirrorRetryAt-Date.now());return}perfGauge('mirrorBuildSliceMs',perfNow()-sliceStarted);scheduleMirrorCheckpoint(0);return}continue}
const next=build.serializer.next();if(next.done){build.serializerDone=true;break}fragment=next.value;fragmentOffset=0;
}
build.fragment=fragment;build.fragmentOffset=fragmentOffset;
if(build.serializerDone){if(build.pendingChunk){if(!writeMirrorBuildChunk(build)){mirrorRetryAt=Date.now()+60*1000;discardMirrorBuild(build);scheduleMirrorCheckpoint(mirrorRetryAt-Date.now());return}perfGauge('mirrorBuildSliceMs',perfNow()-sliceStarted);scheduleMirrorCheckpoint(0);return}build.stage='commit'}
}
perfGauge('mirrorBuildSliceMs',perfNow()-sliceStarted);perfGauge('mirrorBuildSlices',build.slices);
if(hasPendingPersistence()||data.updatedAt!==build.updatedAt||globalChangeSeq!==build.seq){discardMirrorBuild(build);scheduleMirrorCheckpoint();return}
if(build.stage!=='commit'){scheduleMirrorCheckpoint(0);return}
mirrorCheckpointDirty=false;
if(!commitMirrorBuild(build)){mirrorCheckpointDirty=true;mirrorRetryAt=Date.now()+60*1000;discardMirrorBuild(build)}else mirrorBuild=null;
if(mirrorCheckpointDirty)scheduleMirrorCheckpoint(Math.max(MIRROR_IDLE_DELAY,mirrorRetryAt-Date.now()));
}
function scheduleMirrorCheckpoint(delay=MIRROR_IDLE_DELAY){
if(!idbAvailable||activeStorageFormat===FIELD_STORAGE_FORMAT)return;mirrorCheckpointDirty=true;
if(mirrorDelayTimer||mirrorIdleHandle)return;
mirrorDelayTimer=setTimeout(()=>{
mirrorDelayTimer=0;
if(typeof requestIdleCallback==='function')mirrorIdleHandle=requestIdleCallback(runMirrorCheckpoint,{timeout:MIRROR_IDLE_TIMEOUT});
else mirrorIdleHandle=setTimeout(()=>runMirrorCheckpoint({didTimeout:true,timeRemaining:()=>0}),0);
},Math.max(0,delay));
}
async function persistDirtyToDb(options={}){
const{skipCloud=false}=options;
if(options.lifecycle!==true&&interactionActive('persistence')){perfCount('savesDeferredDuringInteraction');await waitForInteractionSettle(null,'persistence')}
if(interactionActive('persistence'))perfCount('persistenceDuringInteraction');
const started=perfStart();pruneAndCount();
if(!hasPendingPersistence()){perfEnd('persistDirtyToDb',started);return{updatedAt:data.updatedAt||data.clockFloor||0,count:0,skipped:true}}
data.worldEpoch=validWorldEpoch(data.worldEpoch)?data.worldEpoch:storedWorldEpoch()||createWorldEpoch();rememberWorldEpoch(data.worldEpoch);
const metaIds=[...dirtyMetaIds],stateIds=[...dirtyStateIds],deleted=[...deletedBoardIds],
deletedVersions=new Map(deleted.map(id=>[id,{rev:deletedBoardRevisions.get(id)||0,revAuthor:deletedBoardAuthors.get(id)||sessionId}])),
outboxDeletes=[...cloudOutboxDeleteKeys],coveredJournals=[...recoveryJournalsToCover],currentRecoverySeq=recoveryJournalSeq,globalSeq=globalChangeSeq,globalChanged=cloudJournalGlobalChanged,
metaVersions=new Map(metaIds.map(id=>[id,revisionVersion(data.metas[id])])),stateVersions=new Map(stateIds.map(id=>[id,revisionVersion(data.states[id])])),
updatedAt=trustedNow(),global=globalForStorage(data,updatedAt),metaRows=metaRowsForStorage(metaIds),stateRows=stateRowsForStorage(stateIds),
metaRowMap=new Map(metaRows.map(row=>[row.id,row])),stateRowMap=new Map(stateRows.map(row=>[row.id,row])),committedMetas=new Map(),committedStates=new Map(),committedDeleted=new Set();
let committedGlobal=global,reconciledPersistenceConflict=false;
if(idbAvailable&&currentRecoverySeq>=0)await recoveryWalPromise;
if(!idbAvailable){
const mirror=snapshotForStorage(updatedAt);
if(!writeCompactMirror(mirror,{authoritative:true}))throw storageAccessError||new Error('\u30d6\u30e9\u30a6\u30b6\u306e\u4fdd\u5b58\u6a5f\u80fd\u3092\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002');
for(const id of deleted)committedDeleted.add(id);
}else{
const db=await openWorldDb(),tx=db.transaction(['control','worlds','boardIndex','boardPuzzles','boardStates','outboxV2','recoveryV2','tombstonesV2'],'readwrite'),done=transactionDone(tx),
controlStore=tx.objectStore('control'),worldsStore=tx.objectStore('worlds'),indexStore=tx.objectStore('boardIndex'),puzzleStore=tx.objectStore('boardPuzzles'),stateV2Store=tx.objectStore('boardStates'),
outboxV2Store=tx.objectStore('outboxV2'),recoveryV2Store=tx.objectStore('recoveryV2'),tombstoneV2Store=tx.objectStore('tombstonesV2'),
affectedIds=[...new Set([...metaIds,...stateIds,...deleted])],
rowReads=affectedIds.map(id=>{const key=[data.worldEpoch,id];return Promise.all([requestValue(indexStore.get(key)),requestValue(puzzleStore.get(key)),requestValue(stateV2Store.get(key)),requestValue(tombstoneV2Store.get(key))])}),
[controlRow,storedWorldRow,existingRows]=await Promise.all([requestValue(controlStore.get('active')),requestValue(worldsStore.get(data.worldEpoch)),Promise.all(rowReads)]),
v2Active=controlRow?.activeFormat===FIELD_STORAGE_FORMAT&&controlRow.activeEpoch===data.worldEpoch;
if(!v2Active||!storedWorldRow||storedWorldRow.status!=='active'){try{tx.abort()}catch(_){}await done.catch(()=>{});throw staleWorldEpochError()}
committedGlobal=mergeGlobalRecords(storedWorldRow.global,global);
for(let index=0;index<affectedIds.length;index++){
const id=affectedIds[index],[existingIndex,existingPuzzle,existingStateRecord,existingTombstone]=existingRows[index],
existingMeta=metaFromV2Records(existingIndex,existingPuzzle),existingState=stateFromV2Record(existingIndex,existingStateRecord),
localMeta=metaRowMap.get(id),localStateRow=stateRowMap.get(id),localState=localStateRow?.value,key=[data.worldEpoch,id];
if(deletedVersions.has(id)){
const version=deletedVersions.get(id),incomingTombstone={id,...version,worldEpoch:data.worldEpoch,deletedAt:updatedAt},tombstone=newerRevisionValue(existingTombstone,incomingTombstone);
if(compareRevisionVersions(tombstone,existingMeta)>=0&&compareRevisionVersions(tombstone,existingState)>=0){
indexStore.delete(key);puzzleStore.delete(key);stateV2Store.delete(key);tombstoneV2Store.put({...tombstone,epoch:data.worldEpoch});
committedMetas.set(id,null);committedStates.set(id,null);committedDeleted.add(id);
}else{committedMetas.set(id,existingMeta||null);committedStates.set(id,existingState||null)}
continue;
}
const metaWinner=localMeta&&(!existingMeta||compareRevisionVersions(localMeta,existingMeta)>=0)?localMeta:existingMeta||null,
tombstoneBlocksMeta=existingTombstone&&compareRevisionVersions(existingTombstone,metaWinner)>=0;
if(tombstoneBlocksMeta||!metaWinner){
if(tombstoneBlocksMeta){indexStore.delete(key);puzzleStore.delete(key);stateV2Store.delete(key);committedDeleted.add(id)}
committedMetas.set(id,null);committedStates.set(id,null);continue;
}
committedMetas.set(id,metaWinner);
const stateWinner=localState&&(!existingState||compareRevisionVersions(localState,existingState)>=0)?localState:existingState||null;
if(stateWinner&&(!existingTombstone||compareRevisionVersions(stateWinner,existingTombstone)>0)){committedStates.set(id,stateWinner);tombstoneV2Store.delete(key)}
else{stateV2Store.delete(key);committedStates.set(id,null)}
const normalizedMeta=normalizeMeta(id,metaWinner),normalizedState=normalizeState(stateWinner);
if(!normalizedMeta){try{tx.abort()}catch(_){}await done.catch(()=>{});throw new Error(`Cannot persist invalid board ${id}.`)}
if(localMeta||!existingPuzzle)puzzleStore.put(puzzleRecordV2(normalizedMeta,data.worldEpoch));
if(localStateRow||!existingStateRecord)stateV2Store.put(stateRecordV2(id,normalizedState,data.worldEpoch));
indexStore.put(summarizeBoardV2(normalizedMeta,normalizedState,data.worldEpoch));tombstoneV2Store.delete(key);
}
for(const key of outboxDeletes)outboxV2Store.delete([data.worldEpoch,key]);
if(cloudApiEnabled){
for(const id of metaIds)if(cloudJournalMetaIds.has(id)){const key=`meta:${id}`,row={key,type:'meta',id,epoch:data.worldEpoch};outboxV2Store.delete([data.worldEpoch,`deleted:${id}`]);outboxV2Store.put(row)}
for(const id of stateIds)if(cloudJournalStateIds.has(id)){const key=`state:${id}`,row={key,type:'state',id,epoch:data.worldEpoch};outboxV2Store.delete([data.worldEpoch,`deleted:${id}`]);outboxV2Store.put(row)}
for(const id of committedDeleted)if(cloudJournalDeletedIds.has(id)){const version=deletedVersions.get(id)||{},key=`deleted:${id}`,row={epoch:data.worldEpoch,key,type:'deleted',id,rev:version.rev||0,revAuthor:version.revAuthor||'',worldEpoch:data.worldEpoch};outboxV2Store.delete([data.worldEpoch,`meta:${id}`]);outboxV2Store.delete([data.worldEpoch,`state:${id}`]);outboxV2Store.put(row)}
for(const id of deleted)if(!committedDeleted.has(id))outboxV2Store.delete([data.worldEpoch,`deleted:${id}`]);
if(globalChanged)outboxV2Store.put({epoch:data.worldEpoch,key:'global',type:'global'});
}
const nextWorld={...storedWorldRow,epoch:data.worldEpoch,status:'active',schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,global:committedGlobal,boardCount:Object.keys(data.metas).length,solvedCount:data.solved||0,score:data.score||0,bounds:fieldBoundsFromMetas(data.metas),createdAt:storedWorldRow.createdAt||updatedAt,activatedAt:storedWorldRow.activatedAt||updatedAt,progress:null};
worldsStore.put(nextWorld);
if(currentRecoverySeq>=0){const key=`covered:${sessionId}`,value={seq:Math.min(currentRecoverySeq,globalSeq),updatedAt,worldEpoch:data.worldEpoch};recoveryV2Store.put({epoch:data.worldEpoch,key,value});recoveryV2Store.delete([data.worldEpoch,`journal:${sessionId}`])}
for(const journal of coveredJournals)if(journal.sessionId&&Number.isSafeInteger(journal.seq)){const key=`covered:${journal.sessionId}`,value={seq:journal.seq,updatedAt,worldEpoch:data.worldEpoch};recoveryV2Store.put({epoch:data.worldEpoch,key,value});if(journal._dbKey)recoveryV2Store.delete([data.worldEpoch,journal._dbKey])}
await done;
updateStorageRevision({databaseUpdatedAt:updatedAt});
scheduleMirrorCheckpoint();
}
for(const[id,version]of metaVersions)if(compareRevisionVersions(data.metas[id],version)===0){
const committed=committedMetas.has(id)?committedMetas.get(id):data.metas[id];
if(committed){if(compareRevisionVersions(committed,version)!==0)reconciledPersistenceConflict=true;data.metas[id]=normalizeMeta(id,committed)||data.metas[id]}
else{reconciledPersistenceConflict=true;delete data.metas[id];delete data.states[id]}
dirtyMetaIds.delete(id);
}
for(const[id,version]of stateVersions)if(compareRevisionVersions(data.states[id],version)===0){
const committed=committedStates.has(id)?committedStates.get(id):data.states[id];
if(committed&&data.metas[id]){if(compareRevisionVersions(committed,version)!==0)reconciledPersistenceConflict=true;data.states[id]=normalizeState(committed);normalizedStateObjects.add(data.states[id])}
else{reconciledPersistenceConflict=true;delete data.states[id]}
dirtyStateIds.delete(id);
}
for(const id of deleted){
const version=deletedVersions.get(id);
if(deletedBoardRevisions.get(id)!==version.rev||deletedBoardAuthors.get(id)!==version.revAuthor)continue;
if(!committedDeleted.has(id)){
const committedMeta=committedMetas.get(id),committedState=committedStates.get(id);
if(committedMeta){
data.metas[id]=normalizeMeta(id,committedMeta)||committedMeta;
if(committedState){data.states[id]=normalizeState(committedState);normalizedStateObjects.add(data.states[id])}
reconciledPersistenceConflict=true;
}
}
deletedBoardIds.delete(id);deletedBoardRevisions.delete(id);deletedBoardAuthors.delete(id);
}
for(const id of deleted)if(!committedDeleted.has(id))cloudJournalDeletedIds.delete(id);
applyGlobalRecordToData(committedGlobal);data.updatedAt=Math.max(data.updatedAt||0,updatedAt);data.clockFloor=Math.max(data.clockFloor||0,updatedAt);
if(activeStorageFormat===FIELD_STORAGE_FORMAT)for(const id of new Set([...metaIds,...stateIds,...deleted])){
if(data.metas[id]&&data.states[id])boardIndexSummaries.set(id,summarizeBoardV2(data.metas[id],data.states[id],data.worldEpoch));else boardIndexSummaries.delete(id);
}
if(reconciledPersistenceConflict)refreshWorldView({rebuild:true,syncConnections:false,persist:false});
for(const key of outboxDeletes)cloudOutboxDeleteKeys.delete(key);
if(globalChangeSeq===globalSeq)globalDirty=false;
clearRecoveryJournalIfCovered(globalSeq,coveredJournals);
if(!skipCloud)scheduleCloudPush({metaIds,stateIds,deleted:[...committedDeleted],globalChanged});
perfGauge('lastPersistRows',metaRows.length+stateRows.length+deleted.length);perfEnd('persistDirtyToDb',started);
return{updatedAt,count:metaRows.length+stateRows.length+deleted.length,skipped:false};
}
async function persistNow(options={}){
if(lifecyclePersistenceSuppressed)return false;
if(options.lifecycle!==true&&interactionActive('persistence')){perfCount('savesDeferredDuringInteraction');await waitForInteractionSettle(null,'persistence')}
clearTimeout(saveTimer);saveTimer=null;
const run=async()=>{if(lifecyclePersistenceSuppressed)return false;try{
const result=await(options.lockHeld===true?persistDirtyToDb(options):withWorldMutationLock(()=>persistDirtyToDb(options)));
if(!result.skipped){setSaveStatus('saved','\u4fdd\u5b58\u3057\u307e\u3057\u305f');hideStatus();if(result.count>0&&startupWorldControl?.activationVerified===false){try{await verifyActiveWorldActivation()}catch(error){const restored=await rollbackUnverifiedWorld(await openWorldDb(),startupWorldControl).catch(()=>null);if(restored){lifecyclePersistenceSuppressed=true;rememberWorldEpoch(restored.activeEpoch);announceWorldReplacement(restored.activeEpoch);location.reload();return false}throw error}}}
if(hasPendingPersistence()){clearTimeout(saveTimer);saveTimer=setTimeout(runDeferredSave,SAVE_DELAY)}
return true;
}catch(error){
if(error?.code==='STALE_WORLD_EPOCH'){lifecyclePersistenceSuppressed=true;setSaveStatus('error','\u5225\u306e\u30bf\u30d6\u3067\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f');showStatus(error.message,{retry:false,fresh:false});setTimeout(()=>location.reload(),80);return false}
setSaveStatus('error','\u4fdd\u5b58\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002');showStatus('\u4fdd\u5b58\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002',{retry:true,fresh:false});console.warn('BEND FIELD: save failed',error);return false}};
if(options.lockHeld===true)return run();
const result=persistQueue.then(run,run);persistQueue=result.then(()=>{},()=>{});return result;
}
function flushSave(options={}){return persistNow(options)}
function runDeferredSave(){
if(interactionActive('persistence')){clearTimeout(saveTimer);saveTimer=setTimeout(runDeferredSave,SAVE_DELAY);perfCount('savesDeferredDuringInteraction');return}
void persistNow();
}
function save(immediate=false,options={}){
if(lifecyclePersistenceSuppressed)return immediate?Promise.resolve(false):false;
if(!hasPendingPersistence())return immediate?Promise.resolve(true):true;
if(immediate)return persistNow(options);
setSaveStatus('saving','\u4fdd\u5b58\u4e2d');clearTimeout(saveTimer);saveTimer=setTimeout(runDeferredSave,SAVE_DELAY);return true;
}
function updatePlayerNameUi(){if(!playerNameBtn)return;const name=currentPlayerName();playerNameBtn.textContent=name;playerNameBtn.title=`プレイヤー名を変更:${name}`}
function normalizePlayerNameInput(value){return String(value??'').replace(/[\u0000-\u001f\u007f]/g,'').replace(/\s+/g,' ').trim().slice(0,24)}
async function commitPlayerProfileName(value){
const requested=normalizePlayerNameInput(value);if(!requested)throw Object.assign(new Error('プレイヤー名を入力してください。'),{code:'INVALID_PLAYER_NAME'});
let committed=requested;
if(cloudAvailable&&data.cloudProfile){
const result=await fetchJson('/api/cloud/profile',{method:'POST',headers:cloudAuthHeaders(),body:JSON.stringify({name:requested})});
committed=normalizePlayerNameInput(result.name)||requested;setCloudStatus();
}
data.playerName=committed;markGlobalDirty(false);
if(!await persistNow({skipCloud:true}))throw new Error('プレイヤー名を端末へ保存できませんでした。');
updatePlayerNameUi();renderAll();schedulePresenceRender(true);return committed;
}
function updateHud(){pruneAndCount();worldCountEl.textContent=Object.keys(data.metas).length;updatePlayerNameUi();updateInventoryUi();updateTimeAttackUi()}
function changed(id,pathIndex=null){const st=metaState(id);st.rev=nextRevision();st.revAuthor=sessionId;markStateDirty(id,pathIndex);save()}
const SHAPES_BY_SIZE=new Map();for(const s of SHAPES){const n=s.length;if(!SHAPES_BY_SIZE.has(n))SHAPES_BY_SIZE.set(n,[]);SHAPES_BY_SIZE.get(n).push(s)}
function shapeCandidatesForLevel(seed,level,attemptsPerSize=12){return AppLogic.shapeCandidatesForLevel(seed,level,attemptsPerSize,{shapesBySize:SHAPES_BY_SIZE,hash32,rngFrom,shuffle})}
function gateConnectionAllowed(meta,gateIndex){
if(!meta?.puzzle||!Number.isInteger(gateIndex))return false;
const puzzle=puzzleOf(meta),gate=puzzle.g[gateIndex],internal=(puzzle.specialCells?.internalGates||[]).some(pair=>pair?.a===gateIndex||pair?.b===gateIndex);return Boolean(gate&&(internal||!(meta.sealedSides||[]).includes(gate[2])));
}
function boardMeta(id){return data.metas[id]}
function puzzleOf(meta){
if(!meta?.puzzle)throw new Error(`\u76e4\u9762${meta?.id||'?'}\u306e\u8aad\u307f\u8fbc\u307f\u304c\u5b8c\u4e86\u3057\u3066\u3044\u307e\u305b\u3093\u3002`);
return meta.puzzle
}
function repairPuzzleDifficulty(meta,{dirty=true}={}){
const puzzle=puzzleOf(meta);if(puzzle.difficulty==null)puzzle.difficulty=solverDifficulty(puzzle,meta.targetLevel??meta.level??1);
if(meta.level===puzzle.difficulty)return false;meta.level=puzzle.difficulty;if(dirty)markMetaDirty(meta.id);return true;
}
function validateHydratedMeta(meta){
if(!meta)throw new Error('\u76e4\u9762\u60c5\u5831\u304c\u3042\u308a\u307e\u305b\u3093\u3002');
if(!meta.puzzle)throw new Error(`\u76e4\u9762${meta.id}\u306e\u30c7\u30fc\u30bf\u304c\u3042\u308a\u307e\u305b\u3093\u3002`);
repairPuzzleDifficulty(meta);sanitizeStateForPuzzle(meta);
if(data.quarantine)delete data.quarantine[meta.id];
return meta;
}
function rememberHydratedBoard(id,bytes){
const previous=hydratedBoardLru.get(id);if(previous)hydratedBoardBytes-=previous.bytes;
hydratedBoardLru.delete(id);hydratedBoardLru.set(id,{bytes});hydratedBoardBytes+=bytes;evictHydratedBoardDetails();
}
function hydratedBoardPinned(id){return id===activeBoard||dirtyMetaIds.has(id)||dirtyStateIds.has(id)||rendered.has(id)||pendingVisibleHydrations?.has?.(id)||openStoreBoardId===id}
function evictHydratedBoardDetails(){
let attempts=0;
while((hydratedBoardLru.size>32||hydratedBoardBytes>8*1024*1024)&&attempts<hydratedBoardLru.size){
const[id,entry]=hydratedBoardLru.entries().next().value;
if(hydratedBoardPinned(id)){hydratedBoardLru.delete(id);hydratedBoardLru.set(id,entry);attempts++;continue}
hydratedBoardLru.delete(id);hydratedBoardBytes-=entry.bytes;const target=data.metas[id],storedSummary=boardIndexSummaries.get(id),currentState=data.states[id],summary=target&&currentState?summarizeBoardV2(target,currentState,data.worldEpoch):storedSummary;
if(target&&summary){boardIndexSummaries.set(id,summary);target.puzzle=null;target._summaryOnly=true;data.states[id]=summaryStateFromIndex(summary);normalizedStateObjects.add(data.states[id]);perfCount('boardDetailEvictions')}
attempts=0;
}
perfGauge('hydratedBoards',hydratedBoardLru.size);perfGauge('hydratedBoardBytes',hydratedBoardBytes);
}
async function hydrateMeta(meta){
if(!meta)throw new Error('Board metadata is missing.');
if(!meta.puzzle){
if(activeStorageFormat!==FIELD_STORAGE_FORMAT||!idbAvailable)throw new Error(`Board details are missing: ${meta.id}`);
const db=await openWorldDb(),tx=db.transaction(['control','boardIndex','boardPuzzles','boardStates'],'readonly'),done=transactionDone(tx),key=[data.worldEpoch,meta.id],
[control,index,puzzle,state]=await Promise.all([requestValue(tx.objectStore('control').get('active')),requestValue(tx.objectStore('boardIndex').get(key)),requestValue(tx.objectStore('boardPuzzles').get(key)),requestValue(tx.objectStore('boardStates').get(key))]);await done;
if(!control||control.activeEpoch!==data.worldEpoch||control.activeFormat!==FIELD_STORAGE_FORMAT)throw staleWorldEpochError();
if(!index||!puzzle||!state||index.metaRev!==puzzle.metaRev||index.stateRev!==state.stateRev)throw new Error(`Board detail revisions do not match: ${meta.id}`);
const loadedMeta=normalizeMeta(meta.id,{id:meta.id,x:index.x,y:index.y,chunks:index.chunks,level:index.level,targetLevel:index.targetLevel,seed:index.seed,axis:index.axis,entrySide:index.entrySide||null,sealedSides:index.sealedSides||[],puzzle:puzzle.puzzle,generatorVersion:puzzle.generatorVersion||GENERATOR_VERSION,rev:puzzle.metaRev,revAuthor:puzzle.revAuthor||''});
if(!loadedMeta)throw new Error(`Stored puzzle is invalid: ${meta.id}`);
const loadedState=normalizeState(state.value);if(loadedState.rev!==index.stateRev)throw new Error(`Board state revision does not match: ${meta.id}`);
const currentState=data.states[meta.id],preserveSolved=Boolean(currentState?.solved&&!loadedState.solved),resolvedState=preserveSolved?preserveSolvedBoardState(currentState,loadedState):loadedState;
if(preserveSolved){resolvedState.rev=nextRevision();resolvedState.revAuthor=sessionId}
boardIndexSummaries.set(meta.id,index);Object.assign(meta,loadedMeta);delete meta._summaryOnly;data.states[meta.id]=resolvedState;normalizedStateObjects.add(resolvedState);rememberStateSignatures(meta.id,resolvedState);if(preserveSolved)markStateDirty(meta.id);
rememberHydratedBoard(meta.id,JSON.stringify(puzzle).length+JSON.stringify(state).length);perfCount('boardDetailHydrations');
}else if(hydratedBoardLru.has(meta.id))rememberHydratedBoard(meta.id,hydratedBoardLru.get(meta.id).bytes);
return validateHydratedMeta(meta);
}
function createMeta(x,y,chunks,level,seed,id=null,puzzle=null){
id=id||'B'+data.nextId++;
if(!puzzle)throw new Error('\u76e4\u9762\u30c7\u30fc\u30bf\u304c\u3042\u308a\u307e\u305b\u3093\u3002');
const p=puzzle;
const meta={id,x,y,chunks,level:p.difficulty,targetLevel:level,seed,axis:p.axis,entrySide:null,sealedSides:[],puzzle:p,generatorVersion:GENERATOR_VERSION,rev:nextRevision()};
data.metas[id]=meta;markMetaDirty(id);ensureMetaState(id,{dirty:false});markStateDirty(id);rebuildOccupancy();
return meta
}
async function ensureStart(){
if(Object.keys(data.metas).length)return;
const chunks=[[0,0]];let puzzle=normalizeStoredPuzzle(deepClone(STARTER_PUZZLE),chunks,1);
if(!puzzle)throw new Error('\u539f\u70b9\u306e\u76e4\u9762\u30c7\u30fc\u30bf\u304c\u58ca\u308c\u3066\u3044\u307e\u3059\u3002');
createMeta(0,0,chunks,1,STARTER_SEED,'B0',puzzle);
data.nextId=Math.max(data.nextId,1);
}
function globalCell(meta,cell){return[meta.y*5+cell[0],meta.x*5+cell[1]]}
function metaAtGlobalCell(r,c){const id=occupancy.get(key2(Math.floor(c/5),Math.floor(r/5)));return id?boardMeta(id)||null:null}
function unitOccupiedAtGlobalCell(r,c,exceptId=null){const meta=metaAtGlobalCell(r,c);return!!(meta&&meta.id!==exceptId)}
async function hydrateAdjacentMetas(meta){
if(!meta?.puzzle)return;
const ids=new Set(),p=puzzleOf(meta);
for(let gateIndex=0;gateIndex<p.g.length;gateIndex++){if((p.specialCells?.internalGates||[]).some(pair=>pair?.a===gateIndex||pair?.b===gateIndex))continue;const g=p.g[gateIndex],[gr,gc]=globalCell(meta,[g[0],g[1]]),[dr,dc]=SIDE_D[g[2]],neighbor=metaAtGlobalCell(gr+dr,gc+dc);if(neighbor&&neighbor.id!==meta.id&&!neighbor.puzzle)ids.add(neighbor.id)}
for(const id of ids){try{await hydrateMeta(data.metas[id])}catch(error){console.warn(`BEND FIELD: adjacent hydration deferred for ${id}`,error)}}
adjacencyCache.clear();
}
function matchingNeighborGate(meta,gi){const cacheKey=meta.id+':'+gi;if(adjacencyCache.has(cacheKey))return adjacencyCache.get(cacheKey);if(!meta?.puzzle||!gateConnectionAllowed(meta,gi)){adjacencyCache.set(cacheKey,null);return null}const p=puzzleOf(meta);if((p.specialCells?.internalGates||[]).some(pair=>pair?.a===gi||pair?.b===gi)){adjacencyCache.set(cacheKey,null);return null}const g=p.g[gi],[gr,gc]=globalCell(meta,[g[0],g[1]]),[dr,dc]=SIDE_D[g[2]],nr=gr+dr,nc=gc+dc,nb=metaAtGlobalCell(nr,nc);if(!nb||nb.id===meta.id||!nb.puzzle){adjacencyCache.set(cacheKey,null);return null}const np=puzzleOf(nb),lr=nr-nb.y*5,lc=nc-nb.x*5;if(!cellSet(np).has(ckey(lr,lc))){adjacencyCache.set(cacheKey,null);return null}const ngi=gateAtCell(np,[lr,lc],null,OPP[g[2]]);const hit=ngi==null||!gateConnectionAllowed(nb,ngi)?null:{meta:nb,gateIndex:ngi};adjacencyCache.set(cacheKey,hit);return hit}
function pathUsesGate(path,gi){return Boolean(path&&((!path.detachedStart&&path.startGate===gi)||path.endGate===gi||path.openGate===gi))}
function gateUsedPath(st,gi){return st.paths.find(path=>path&&Array.isArray(path.cells)&&pathUsesGate(path,gi))||null}
function pathColorIndexAtGate(path,gi){
if(!path)return null;
if(!path.detachedStart&&path.startGate===gi)return path.startColorIndex??path.colorIndex??0;
if(path.endGate===gi||path.openGate===gi)return path.endColorIndex??path.colorIndex??path.startColorIndex??0;
return path.colorIndex??0;
}
function setPathColorAtGate(path,gi,colorIndex){
if(!path||colorIndex==null)return false;
const key=!path.detachedStart&&path.startGate===gi?'startColorIndex':path.endGate===gi||path.openGate===gi?'endColorIndex':null;
if(!key||path[key]===colorIndex)return false;
path[key]=colorIndex;
if(key==='startColorIndex')path.colorIndex=colorIndex;
return true;
}
function normalizeLineColorIndex(value){return Number.isInteger(value)?((value%LINE_COLORS.length)+LINE_COLORS.length)%LINE_COLORS.length:null}
function boundaryColorSource(meta,gi,hit=matchingNeighborGate(meta,gi)){
if(!hit)return null;
const st=metaState(meta.id),neighborState=metaState(hit.meta.id),own=gateUsedPath(st,gi),other=gateUsedPath(neighborState,hit.gateIndex),candidates=[];
if(own)candidates.push({meta,gateIndex:gi,state:st,path:own,colorIndex:normalizeLineColorIndex(pathColorIndexAtGate(own,gi))});
if(other)candidates.push({meta:hit.meta,gateIndex:hit.gateIndex,state:neighborState,path:other,colorIndex:normalizeLineColorIndex(pathColorIndexAtGate(other,hit.gateIndex))});
const usable=candidates.filter(candidate=>candidate.colorIndex!=null);if(!usable.length)return null;
usable.sort((a,b)=>Number(b.state.solved)-Number(a.state.solved)||(a.state.rev||0)-(b.state.rev||0)||String(a.meta.id).localeCompare(String(b.meta.id))||a.gateIndex-b.gateIndex);
return usable[0];
}
function confirmedBoundaryColorIndex(meta,gi){
const hit=matchingNeighborGate(meta,gi);if(!hit)return null;
const st=metaState(meta.id),neighborState=metaState(hit.meta.id),own=gateUsedPath(st,gi),other=gateUsedPath(neighborState,hit.gateIndex);
if(!own&&!other)return null;
if(!st.solved&&!neighborState.solved)return null;
return boundaryColorSource(meta,gi,hit)?.colorIndex??null;
}
function sharedBoundaryColorIndex(meta,gi){const hit=matchingNeighborGate(meta,gi);return hit?boundaryColorSource(meta,gi,hit)?.colorIndex??null:null}
function displayedEndpointColorIndex(meta,path,side='end'){if(!path)return 0;let gateIndex=null;if(side==='start'&&!path.detachedStart)gateIndex=path.startGate;else if(side==='end'){if(Number.isInteger(path.openGate))gateIndex=path.openGate;else if(Number.isInteger(path.endGate))gateIndex=path.endGate;else if(path.cells?.length===1&&!path.detachedStart)gateIndex=path.startGate}const shared=Number.isInteger(gateIndex)?sharedBoundaryColorIndex(meta,gateIndex):null;if(shared!=null)return shared;return side==='start'?(path.startColorIndex??path.colorIndex??0):(path.endColorIndex??path.startColorIndex??path.colorIndex??0)}
function neighborColor(meta,gi){return sharedBoundaryColorIndex(meta,gi)}
function addInheritedBoundaryPath(meta,gi,colorIndex){
const st=metaState(meta.id);if(st.solved||gateUsedPath(st,gi))return false;
const p=puzzleOf(meta),g=gateObj(p,gi),occ=occupiedMap(st);if(occ.has(ckey(...g.cell)))return false;
st.paths.push({startGate:gi,endGate:null,openGate:null,cells:[[g.cell[0],g.cell[1]]],colorIndex,startColorIndex:colorIndex,endColorIndex:null});return true;
}
function syncBoundaryConnections({rebuild=true,persist=true,updateProgress=true}={}){
const touched=new Set(),processed=new Set();
if(rebuild)rebuildOccupancy();
const candidateIds=new Set();
if(!rebuild&&dirtyStateIds.size){
for(const id of dirtyStateIds){
const meta=data.metas[id];if(!meta)continue;candidateIds.add(id);
for(const[dx,dy]of meta.chunks||[]){const unitX=meta.x+dx,unitY=meta.y+dy;for(const[dr,dc]of Object.values(SIDE_D)){const neighborId=occupancy.get(key2(unitX+dc,unitY+dr));if(neighborId)candidateIds.add(neighborId)}}
}
}else for(const id of Object.keys(data.metas))candidateIds.add(id);
for(const id of candidateIds){
const meta=data.metas[id];
if(!meta?.puzzle)continue;
const st=metaState(meta.id),p=puzzleOf(meta);
for(let gi=0;gi<p.g.length;gi++){
const hit=matchingNeighborGate(meta,gi);if(!hit)continue;
const ends=[`${meta.id}:${gi}`,`${hit.meta.id}:${hit.gateIndex}`].sort(),pairKey=ends.join('|');if(processed.has(pairKey))continue;processed.add(pairKey);
const nst=metaState(hit.meta.id),own=gateUsedPath(st,gi),other=gateUsedPath(nst,hit.gateIndex);
if(!own&&!other)continue;
const source=boundaryColorSource(meta,gi,hit);if(!source)continue;const colorIndex=source.colorIndex;
if(own&&setPathColorAtGate(own,gi,colorIndex))touched.add(meta.id);
if(other&&setPathColorAtGate(other,hit.gateIndex,colorIndex))touched.add(hit.meta.id);
if(!own&&nst.solved&&other&&addInheritedBoundaryPath(meta,gi,colorIndex))touched.add(meta.id);
if(!other&&st.solved&&own&&addInheritedBoundaryPath(hit.meta,hit.gateIndex,colorIndex))touched.add(hit.meta.id);
}
}
if(touched.size){
const now=Date.now();let k=0;
for(const id of touched){metaState(id).rev=Math.max(nextRevision(),now+(++k));markStateDirty(id)}
if(persist)save();
}
if(updateProgress)updateSelectedProgress(rendered.get(activeBoard));
return touched.size;
}
function refreshWorldView(options={}){
if(interactionActive('world'))return waitForInteractionSettle(undefined,'world').then(()=>refreshWorldView(options));
const{rebuild=true,syncConnections=true,markStats=false,resumeTimer=false,hide=false,persist=false,immediate=false,lockHeld=false}=options;
if(rebuild)rebuildOccupancy();
if(syncConnections)syncBoundaryConnections({rebuild:false,persist:false,updateProgress:false});
if(markStats)statsDirty=true;
ensureBoards();renderAll();updateHud();updateSelectedProgress(rendered.get(activeBoard));
if(resumeTimer)resumeTimeAttackTimer();if(hide)hideStatus();
return persist?save(immediate,{lockHeld}):true;
}
function waitForInteractionSettle(counter='worldRefreshesDeferredDuringInteraction',scope='any'){
if(!interactionActive(scope))return Promise.resolve();
if(counter)perfCount(counter);
return new Promise(resolve=>{const unsubscribe=interactionState.subscribe(()=>{if(!interactionActive(scope)){unsubscribe();resolve()}})});
}
function fitsMeta(x,y,chunks){
for(const[dx,dy]of chunks)if(occupancy.has(key2(x+dx,y+dy)))return false;
return true;
}
function placedShapeKeys(x,y,chunks){return new Set(chunks.map(([dx,dy])=>key2(x+dx,y+dy)))}
function unitOccupiedWithExtra(unitX,unitY,extraOccupied=null){const key=key2(unitX,unitY);return occupancy.has(key)||extraOccupied?.has(key)}
function isClosedVoidWithExtra(unitX,unitY,extraOccupied=null){
if(unitOccupiedWithExtra(unitX,unitY,extraOccupied))return false;
let count=0;for(const[dr,dc]of Object.values(SIDE_D))if(unitOccupiedWithExtra(unitX+dc,unitY+dr,extraOccupied))count++;
return count===4;
}
function openUnitFrontiers(meta,extraOccupied=null){
if(!meta?.chunks?.length)return[];
const own=new Set(meta.chunks.map(([dx,dy])=>key2(meta.x+dx,meta.y+dy))),frontiers=new Map();
for(const[dx,dy]of meta.chunks){
const unitX=meta.x+dx,unitY=meta.y+dy;
for(const side of['N','S','W','E']){
const[dr,dc]=SIDE_D[side],targetX=unitX+dc,targetY=unitY+dr,key=key2(targetX,targetY);
if(own.has(key)||unitOccupiedWithExtra(targetX,targetY,extraOccupied)||isClosedVoidWithExtra(targetX,targetY,extraOccupied))continue;
if(!frontiers.has(key))frontiers.set(key,{unitX:targetX,unitY:targetY,side,targetSide:OPP[side]});
}
}
return[...frontiers.values()];
}
function shapeFitsWithExtra(x,y,chunks,extraOccupied=null){
for(const[dx,dy]of chunks){const key=key2(x+dx,y+dy);if(occupancy.has(key)||extraOccupied?.has(key))return false}
return true;
}
function viableFuturePlacementAt(unitX,unitY,extraOccupied=null){
if(unitOccupiedWithExtra(unitX,unitY,extraOccupied)||isClosedVoidWithExtra(unitX,unitY,extraOccupied))return false;
const targetLevel=macroDifficulty(unitX,unitY),
seed=hash32(Math.imul(unitX+0x3191,73856093)^Math.imul(unitY-0x47b5,19349663)^0x51ed270b),
candidates=shapeCandidatesForLevel(seed,targetLevel,5);
for(const shape of candidates)for(const[dx,dy]of shape)if(shapeFitsWithExtra(unitX-dx,unitY-dy,shape,extraOccupied))return true;
return false;
}
function viableExpansionExists(meta,extraOccupied=null){
for(const frontier of openUnitFrontiers(meta,extraOccupied))if(viableFuturePlacementAt(frontier.unitX,frontier.unitY,extraOccupied))return true;
return false;
}
function prospectiveShapeHasFrontier(x,y,chunks){
const own=placedShapeKeys(x,y,chunks),prospective={x,y,chunks};
return viableExpansionExists(prospective,own);
}
function placementPreservesUnsolvedFrontiers(x,y,chunks,sourceId=null,{allowTerminal=false}={}){
const extra=placedShapeKeys(x,y,chunks);
const terminalFill=allowTerminal&&chunks.length===1&&isClosedVoidUnit(x,y),
connectedMetaIds=terminalFill?new Set(placementConnectionRequirements(x,y,chunks).map(requirement=>requirement.metaId)):null;
if(!terminalFill&&!prospectiveShapeHasFrontier(x,y,chunks))return false;
const affectedIds=new Set();
for(const key of extra){const[unitX,unitY]=key.split(',').map(Number);for(const[dr,dc]of Object.values(SIDE_D)){const id=occupancy.get(key2(unitX+dc,unitY+dr));if(id)affectedIds.add(id)}}
for(const id of affectedIds){
const meta=data.metas[id];
if(!meta?.chunks?.length||meta.id===sourceId)continue;const state=metaState(meta.id);
if(state.solved&&state.expanded)continue;
const viableBefore=viableExpansionExists(meta);
if(viableBefore&&!viableExpansionExists(meta,extra)&&!connectedMetaIds?.has(meta.id))return false;
}
return true;
}
function storeWorldCenter(meta){
const count=Math.max(1,meta.chunks.length),
x=meta.x+.5+meta.chunks.reduce((sum,chunk)=>sum+chunk[0],0)/count,
y=meta.y+.5+meta.chunks.reduce((sum,chunk)=>sum+chunk[1],0)/count;
return[x,y];
}
function worldSeedValue(){return data.metas.B0?.seed??STARTER_SEED}
function storePriceLocation(meta,store){
const direct=Array.isArray(store?.cell)?store.cell:null,path=store&&metaState(meta.id).paths[store.pathIndex],cell=direct||path?.cells?.[store.cellIndex];
return cell?[meta.x+(cell[1]+.5)/CHUNK,meta.y+(cell[0]+.5)/CHUNK]:storeWorldCenter(meta);
}
function storeCellForMeta(meta){
const state=meta&&metaState(meta.id),store=state?.store,direct=Array.isArray(store?.cell)?store.cell:null,path=store&&state.paths[store.pathIndex],cell=direct||path?.cells?.[store.cellIndex];
return Array.isArray(cell)?cell:Array.isArray(store?.summaryCell)?store.summaryCell:null;
}
function storePriceDetails(meta,store){
const[x,y]=storePriceLocation(meta,store),priceVersion=store?.priceVersion||STORE_PRICE_VERSION,
computed=AppLogic.deterministicStorePrice(1,worldSeedValue(),x,y,priceVersion),
coefficient=Number.isFinite(store?.priceCoefficient)?store.priceCoefficient:computed.coefficient;
return{coefficient,priceVersion,x,y};
}
function storeItemPrice(meta,store,item){
const coefficient=storePriceDetails(meta,store).coefficient,baseCost=Math.max(0,Number(item?.cost)||0);
const adjusted=Math.round(baseCost*coefficient);
if(item?.id?.startsWith('cursor-face-'))return Math.max(MIN_CURSOR_PRICE,Math.min(MAX_FACE_CURSOR_PRICE,adjusted));
return Math.max(item?.cursorStyle?MIN_CURSOR_PRICE:MIN_STORE_ITEM_PRICE,adjusted);
}
let storeEffectSourcesCache=null,storeEffectSpatialCache=null;
function targetLevelForNewBoard(x,y,chunks){
const count=Math.max(1,chunks.length),centerX=x+.5+chunks.reduce((sum,chunk)=>sum+chunk[0],0)/count,centerY=y+.5+chunks.reduce((sum,chunk)=>sum+chunk[1],0)/count;
return macroDifficulty(centerX,centerY);
}
let worldMutationQueue=Promise.resolve();
function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))}
async function withStorageLease(name,task){
const key=`${name}:lease`,token=`${sessionId}:${Math.random().toString(36).slice(2)}`,deadline=Date.now()+20000;
if(storageAccessError)return task();
while(Date.now()<deadline){
const now=Date.now();let existing=null;try{existing=JSON.parse(safeLocalGet(key)||'null')}catch(_){}
if(!existing||existing.expiresAt<now||existing.token===token){safeLocalSet(key,JSON.stringify({token,expiresAt:now+15000}));let check=null;try{check=JSON.parse(safeLocalGet(key)||'null')}catch(_){}if(check?.token===token){
const renew=setInterval(()=>{let current=null;try{current=JSON.parse(safeLocalGet(key)||'null')}catch(_){}if(current?.token===token)safeLocalSet(key,JSON.stringify({token,expiresAt:Date.now()+15000}))},5000);
try{return await task()}finally{clearInterval(renew);let current=null;try{current=JSON.parse(safeLocalGet(key)||'null')}catch(_){}if(current?.token===token)safeLocalRemove(key)}
}}
await sleep(35+Math.random()*90);
}
throw new Error('\u5225\u306e\u30bf\u30d6\u3067\u66f4\u65b0\u3057\u3066\u3044\u307e\u3059\u3002');
}
function withWorldMutationLock(task){
return navigator.locks?.request?navigator.locks.request(worldLockName,{mode:'exclusive'},task):withStorageLease(worldLockName,task);
}
function enqueueWorldMutation(task){
const run=()=>withWorldMutationLock(task);
const result=worldMutationQueue.then(run,run);worldMutationQueue=result.catch(()=>{});return result;
}
function occupiedNeighborCount(unitX,unitY){
let count=0;
for(const[dx,dy]of[[0,-1],[0,1],[-1,0],[1,0]])if(occupancy.has(key2(unitX+dx,unitY+dy)))count++;
return count;
}
function isClosedVoidUnit(unitX,unitY){return!occupancy.has(key2(unitX,unitY))&&occupiedNeighborCount(unitX,unitY)===4}
function frontierCandidates(meta){
if(!meta)return[];
const own=new Set(meta.chunks.map(([dx,dy])=>key2(meta.x+dx,meta.y+dy))),map=new Map();
for(const[dx,dy]of meta.chunks){
const unitX=meta.x+dx,unitY=meta.y+dy;
for(const side of['N','S','W','E']){
const[dr,dc]=SIDE_D[side],targetX=unitX+dc,targetY=unitY+dr,key=key2(targetX,targetY);
if(own.has(key)||occupancy.has(key)||isClosedVoidUnit(targetX,targetY))continue;
const existing=map.get(key)||{unitX:targetX,unitY:targetY,side,targetSide:OPP[side],contacts:[]};
existing.contacts.push({unitX,unitY,side});map.set(key,existing);
}
}
return[...map.values()].sort((a,b)=>{
const ah=hash32(meta.seed^Math.imul(a.unitX+8191,73856093)^Math.imul(a.unitY-12289,19349663)),bh=hash32(meta.seed^Math.imul(b.unitX+8191,73856093)^Math.imul(b.unitY-12289,19349663));
return(ah>>>0)-(bh>>>0)||a.unitY-b.unitY||a.unitX-b.unitX;
});
}
function repairFacingGateConnections(meta){
if(!meta?.puzzle)return 0;
const p=puzzleOf(meta);let changed=0;
for(let gateIndex=0;gateIndex<p.g.length;gateIndex++){
if((p.specialCells?.internalGates||[]).some(pair=>pair?.a===gateIndex||pair?.b===gateIndex)||!gateConnectionAllowed(meta,gateIndex))continue;
const g=p.g[gateIndex],[gr,gc]=globalCell(meta,[g[0],g[1]]),[dr,dc]=SIDE_D[g[2]],nr=gr+dr,nc=gc+dc,
neighbor=metaAtGlobalCell(nr,nc);
if(!neighbor||neighbor.id===meta.id||!neighbor.puzzle)continue;
const np=puzzleOf(neighbor),lr=nr-neighbor.y*5,lc=nc-neighbor.x*5,neighborGate=gateAtCell(np,[lr,lc],null,OPP[g[2]]);
if(neighborGate==null||(neighbor.sealedSides||[]).includes(OPP[g[2]])===false)continue;
neighbor.sealedSides=neighbor.sealedSides.filter(side=>side!==OPP[g[2]]);
neighbor.rev=nextRevision();markMetaDirty(neighbor.id);changed++;
}
if(changed)adjacencyCache.clear();
return changed;
}
function gateFrontierCandidates(meta){
if(!meta?.puzzle)return[];
const p=puzzleOf(meta),map=new Map();
for(let gateIndex=0;gateIndex<p.g.length;gateIndex++){
if((p.specialCells?.internalGates||[]).some(pair=>pair?.a===gateIndex||pair?.b===gateIndex)||!gateConnectionAllowed(meta,gateIndex)||matchingNeighborGate(meta,gateIndex))continue;
const g=p.g[gateIndex],[gr,gc]=globalCell(meta,[g[0],g[1]]),[dr,dc]=SIDE_D[g[2]],nr=gr+dr,nc=gc+dc,
targetUnitX=Math.floor(nc/5),targetUnitY=Math.floor(nr/5),key=key2(targetUnitX,targetUnitY);
if(occupancy.has(key))continue;
const existing=map.get(key)||{unitX:targetUnitX,unitY:targetUnitY,side:g[2],targetSide:OPP[g[2]],contacts:[],gateDriven:true,terminalFill:isClosedVoidUnit(targetUnitX,targetUnitY)};
existing.contacts.push({unitX:Math.floor(gc/5),unitY:Math.floor(gr/5),side:g[2],gateIndex,globalRow:gr,globalCol:gc});
map.set(key,existing);
}
return[...map.values()].sort((a,b)=>{
const ai=Math.min(...a.contacts.map(contact=>contact.gateIndex)),bi=Math.min(...b.contacts.map(contact=>contact.gateIndex));
return ai-bi||a.unitY-b.unitY||a.unitX-b.unitX;
});
}
function unresolvedExpansionCandidates(meta){return gateFrontierCandidates(meta)}
function closedVoidRepairCandidates(){
const candidates=[];
for(const key of closedVoidKeys){
const[unitX,unitY]=key.split(',').map(Number);if(!isClosedVoidUnit(unitX,unitY)){closedVoidKeys.delete(key);continue}
let candidate=null;
for(const side of['N','S','W','E']){
const[dr,dc]=SIDE_D[side],sourceId=occupancy.get(key2(unitX-dc,unitY-dr)),source=data.metas[sourceId];
if(!source?.chunks?.length||!metaState(source.id).solved)continue;
const gateFrontier=gateFrontierCandidates(source).find(frontier=>frontier.unitX===unitX&&frontier.unitY===unitY);
candidate={source,frontier:gateFrontier||{unitX,unitY,side,targetSide:OPP[side],contacts:[],gateDriven:false,terminalFill:true,closedVoidRepair:true}};break;
}
if(candidate)candidates.push(candidate);
}
return candidates.sort((left,right)=>left.frontier.unitY-right.frontier.unitY||left.frontier.unitX-right.frontier.unitX||left.source.id.localeCompare(right.source.id,undefined,{numeric:true}));
}
function missingGateConnections(meta){
if(!meta?.puzzle)return[];
const p=puzzleOf(meta),missing=[];
for(let gateIndex=0;gateIndex<p.g.length;gateIndex++){
if((p.specialCells?.internalGates||[]).some(pair=>pair?.a===gateIndex||pair?.b===gateIndex)||!gateConnectionAllowed(meta,gateIndex)||matchingNeighborGate(meta,gateIndex))continue;
const g=p.g[gateIndex],[gr,gc]=globalCell(meta,[g[0],g[1]]),[dr,dc]=SIDE_D[g[2]],nr=gr+dr,nc=gc+dc,
blocker=metaAtGlobalCell(nr,nc);
missing.push({gateIndex,side:g[2],globalRow:gr,globalCol:gc,neighborRow:nr,neighborCol:nc,blockedBy:blocker?.id||null});
}
return missing;
}
function placementConnectionRequirements(x,y,chunks){
const placed=placedShapeKeys(x,y,chunks),requirements=[],neighborIds=new Set();
for(const key of placed){const[unitX,unitY]=key.split(',').map(Number);for(const[dr,dc]of Object.values(SIDE_D)){const id=occupancy.get(key2(unitX+dc,unitY+dr));if(id)neighborIds.add(id)}}
for(const id of neighborIds){
const meta=data.metas[id];
if(!meta?.puzzle)continue;
const p=puzzleOf(meta);
for(let gateIndex=0;gateIndex<p.g.length;gateIndex++){
if((p.specialCells?.internalGates||[]).some(pair=>pair?.a===gateIndex||pair?.b===gateIndex)||!gateConnectionAllowed(meta,gateIndex)||matchingNeighborGate(meta,gateIndex))continue;
const g=p.g[gateIndex],[gr,gc]=globalCell(meta,[g[0],g[1]]),[dr,dc]=SIDE_D[g[2]],nr=gr+dr,nc=gc+dc,
targetUnitX=Math.floor(nc/5),targetUnitY=Math.floor(nr/5);
if(!placed.has(key2(targetUnitX,targetUnitY)))continue;
requirements.push({metaId:meta.id,gateIndex,sourceSide:g[2],targetSide:OPP[g[2]],neighborRow:nr,neighborCol:nc});
}
}
return requirements;
}
function puzzleSupportsConnectionRequirements(puzzle,x,y,requirements){
if(!requirements.length)return true;
const valid=cellSet(puzzle);
for(const requirement of requirements){
const localRow=requirement.neighborRow-y*5,localCol=requirement.neighborCol-x*5;
if(!valid.has(ckey(localRow,localCol)))return false;
if(gateAtCell(puzzle,[localRow,localCol],null,requirement.targetSide)==null)return false;
}
return true;
}
function sameNumberList(a,b){return a.length===b.length&&a.every((value,index)=>value===b[index])}
function fixedPortProfilesForRequirements(requirements){
const groups=new Map();
for(const requirement of requirements){
const unitX=Math.floor(requirement.neighborCol/5),unitY=Math.floor(requirement.neighborRow/5),side=requirement.targetSide;
let key,offset,profiles;
if(side==='N'||side==='S'){
key=horizontalBoundaryKey(unitX,side==='N'?unitY:unitY+1);offset=requirement.neighborCol-unitX*5;profiles=H_PORT_PROFILES;
}else{
key=verticalBoundaryKey(side==='W'?unitX:unitX+1,unitY);offset=requirement.neighborRow-unitY*5;profiles=V_PORT_PROFILES;
}
const group=groups.get(key)||{profiles,offsets:new Set()};group.offsets.add(offset);groups.set(key,group);
}
const fixed={};
for(const[key,{profiles,offsets}]of groups){
const values=[...offsets].sort((a,b)=>a-b),index=profiles.findIndex(profile=>sameNumberList(profile,values));
if(index<0)return null;
fixed[key]=index;
}
return fixed;
}
function generatedPuzzleIssue(puzzle){
if(!puzzle||!Array.isArray(puzzle.valid)||!Array.isArray(puzzle.g)||!Array.isArray(puzzle.n)||!Array.isArray(puzzle.solution))return'\u76e4\u9762\u30c7\u30fc\u30bf\u4e0d\u8db3';
if(!puzzle.valid.length||!puzzle.g.length||puzzle.g.length%2||puzzle.solution.length!==puzzle.n.length)return'\u7dda\u3068\u30b2\u30fc\u30c8\u306e\u6570\u304c\u4e0d\u6b63';
const special=specialCellSet(puzzle),crossingCoverage=new Set(special.crossings.map(cell=>ckey(...cell))),valid=new Set(),coverage=new Map(),usedGates=new Set();
for(const cell of puzzle.valid){const key=ckey(...cell);if(valid.has(key))return'\u6709\u52b9\u30bb\u30eb\u91cd\u8907';valid.add(key)}
for(const path of puzzle.solution){
if(!path||!Array.isArray(path.cells)||!path.cells.length||!Number.isInteger(path.startGate)||!Number.isInteger(path.endGate)||path.startGate===path.endGate)return'\u89e3\u7b54\u7dda\u4e0d\u6b63';
if(path.startGate<0||path.endGate<0||path.startGate>=puzzle.g.length||path.endGate>=puzzle.g.length||usedGates.has(path.startGate)||usedGates.has(path.endGate))return'\u30b2\u30fc\u30c8\u91cd\u8907';
usedGates.add(path.startGate);usedGates.add(path.endGate);
if(!sameCell(path.cells[0],puzzle.g[path.startGate])||!sameCell(path.cells[path.cells.length-1],puzzle.g[path.endGate]))return'\u89e3\u7b54\u7dda\u3068\u30b2\u30fc\u30c8\u306e\u4e0d\u6574\u5408';
const own=new Set();
for(let index=0;index<path.cells.length;index++){
const cell=path.cells[index],key=ckey(...cell);if(!valid.has(key)||own.has(key))return'\u89e3\u7b54\u7dda\u30bb\u30eb\u4e0d\u6b63';
if(index&& !pathCellsAdjacent(puzzle,path.cells[index-1],cell))return'\u89e3\u7b54\u7dda\u304c\u5206\u65ad';
own.add(key);coverage.set(key,(coverage.get(key)||0)+1);
}
if(!pathValid(path,puzzle))return'\u66f2\u304c\u308a\u6570\u307e\u305f\u306f\u7279\u6b8a\u30bb\u30eb\u304c\u4e0d\u6574\u5408';
}
if(usedGates.size!==puzzle.g.length)return'\u672a\u4f7f\u7528\u30b2\u30fc\u30c8';
for(const key of valid)if((coverage.get(key)||0)!==(crossingCoverage.has(key)?2:1))return'\u672a\u88ab\u8986\u30bb\u30eb';
for(const cell of special.crossings){
if(!valid.has(ckey(...cell)))return'\u4ea4\u5dee\u30bb\u30eb\u304c\u76e4\u9762\u5916';
for(const neighbor of[[cell[0]-1,cell[1]],[cell[0]+1,cell[1]],[cell[0],cell[1]-1],[cell[0],cell[1]+1]])if(!valid.has(ckey(...neighbor)))return'\u4ea4\u5dee\u30bb\u30eb\u304c\u5916\u5468';
}
for(const pair of special.warps)if(!valid.has(ckey(...pair.a))||!valid.has(ckey(...pair.b)))return'\u30ef\u30fc\u30d7\u30bb\u30eb\u304c\u76e4\u9762\u5916';
for(const lock of special.locks)if(!valid.has(ckey(...lock.key))||!valid.has(ckey(...lock.door)))return'\u9375\u307e\u305f\u306f\u6249\u304c\u76e4\u9762\u5916';
const internalSeen=new Set();for(const pair of special.internalGates||[]){const a=pair?.a,b=pair?.b,ga=puzzle.g[a],gb=puzzle.g[b];if(!Number.isInteger(a)||!Number.isInteger(b)||a===b||!ga||!gb||internalSeen.has(a)||internalSeen.has(b))return'盤面内ゲートが不正';const da=SIDE_D[ga[2]],db=SIDE_D[gb[2]];if(manhattan(ga,gb)!==1||ga[0]+da[0]!==gb[0]||ga[1]+da[1]!==gb[1]||gb[0]+db[0]!==ga[0]||gb[1]+db[1]!==ga[1])return'盤面内ゲートの向きが不正';internalSeen.add(a);internalSeen.add(b)}
return null;
}
function obstacleDetours(puzzle){
const candidates=[];
for(let pathIndex=0;pathIndex<puzzle.solution.length;pathIndex++){
const cells=puzzle.solution[pathIndex].cells;
for(let start=1;start<cells.length-2;start++)for(let end=start;end<Math.min(cells.length-1,start+8);end++){
if(manhattan(cells[start-1],cells[end+1])!==1)continue;
const nextCells=[...cells.slice(0,start),...cells.slice(end+1)],test={...puzzle.solution[pathIndex],cells:nextCells};
if(nextCells.length<2||turnAnalysis(test,puzzle).count<1)continue;
candidates.push({pathIndex,start,end,cells:cells.slice(start,end+1)});
}
}
return candidates;
}
function obstacleCellLimit(puzzle){
const total=(puzzle?.valid?.length||0)+(puzzle?.obstacles?.length||0);
return Math.max(0,Math.floor(total*.2));
}
function addObstaclePattern(sourcePuzzle,seed){
const puzzle=deepClone(sourcePuzzle),rng=rngFrom(hash32(seed^0x4f1bbcdc)),planned=new Set(),allCells=shuffle([...puzzle.valid],rng),
totalCells=allCells.length,largePuzzleBoost=Math.min(.04,Math.max(0,totalCells-25)/225*.04),baseRate=.02+largePuzzleBoost,maxObstacles=obstacleCellLimit(puzzle);
for(const[r,c]of allCells){
if(planned.size>=maxObstacles)break;
const adjacent=[[1,0],[-1,0],[0,1],[0,-1]].some(([dr,dc])=>planned.has(ckey(r+dr,c+dc))),rate=Math.min(.18,baseRate+(adjacent?.09:0));
if(rng()<rate)planned.add(ckey(r,c));
}
const targetCount=Math.min(planned.size,maxObstacles);if(!targetCount){puzzle.obstacles=[];return puzzle}
const obstacles=new Map();
for(let round=0;round<targetCount*3&&obstacles.size<targetCount;round++){
const remaining=targetCount-obstacles.size,allCandidates=obstacleDetours(puzzle);let candidates=allCandidates.filter(candidate=>candidate.cells.length<=remaining);
if(!candidates.length)break;
candidates.sort((a,b)=>{
const aPlanned=a.cells.reduce((sum,cell)=>sum+(planned.has(ckey(...cell))?1:0),0),bPlanned=b.cells.reduce((sum,cell)=>sum+(planned.has(ckey(...cell))?1:0),0),
aAdjacent=a.cells.some(([r,c])=>[[1,0],[-1,0],[0,1],[0,-1]].some(([dr,dc])=>obstacles.has(ckey(r+dr,c+dc))))?1:0,
bAdjacent=b.cells.some(([r,c])=>[[1,0],[-1,0],[0,1],[0,-1]].some(([dr,dc])=>obstacles.has(ckey(r+dr,c+dc))))?1:0;
return(bPlanned*100+bAdjacent*12-b.cells.length)-(aPlanned*100+aAdjacent*12-a.cells.length)||a.cells.length-b.cells.length||rng()-.5;
});
const chosen=candidates[0],path=puzzle.solution[chosen.pathIndex];path.cells=[...path.cells.slice(0,chosen.start),...path.cells.slice(chosen.end+1)];for(const cell of chosen.cells)obstacles.set(ckey(...cell),cell);
}
if(!obstacles.size){puzzle.obstacles=[];return puzzle}
puzzle.obstacles=[...obstacles.values()];puzzle.valid=puzzle.valid.filter(cell=>!obstacles.has(ckey(...cell)));puzzle.n=[];let maxTurns=0,totalTurns=0;
const clueRng=rngFrom(hash32(seed^0x2c1b3c6d));
for(const path of puzzle.solution){const analysis=turnAnalysis(path,puzzle);if(!analysis.count||!analysis.cells.length)return sourcePuzzle;const clue=analysis.cells[Math.floor(clueRng()*analysis.cells.length)];puzzle.n.push([clue[0],clue[1],analysis.count]);maxTurns=Math.max(maxTurns,analysis.count);totalTurns+=analysis.count}
puzzle.maxTurns=maxTurns;puzzle.totalTurns=totalTurns;puzzle.style=`${puzzle.style||'procedural'}-obstacles`;
puzzle.difficulty=sourcePuzzle.difficulty;puzzle.level=sourcePuzzle.level??sourcePuzzle.difficulty
return puzzle;
}
function specialCellSet(p){
if(!p.specialCells)p.specialCells={crossings:[],warps:[],locks:[],internalGates:[]};
if(!Array.isArray(p.specialCells.crossings))p.specialCells.crossings=[];
if(!Array.isArray(p.specialCells.warps))p.specialCells.warps=[];
if(!Array.isArray(p.specialCells.locks))p.specialCells.locks=[];
if(!Array.isArray(p.specialCells.internalGates))p.specialCells.internalGates=[];
return p.specialCells;
}
function internalGateIndexes(p){
const indexes=[];for(const pair of specialCellSet(p).internalGates||[]){if(Number.isInteger(pair?.a))indexes.push(pair.a);if(Number.isInteger(pair?.b))indexes.push(pair.b)}return indexes;
}
function internalGateIndexSet(p){if(!p)return new Set();return p._internalGateIndexes||(p._internalGateIndexes=new Set(internalGateIndexes(p)))}
function isInternalGateIndex(p,index){return internalGateIndexSet(p).has(index)}
function internalGatePartner(p,index){for(const pair of specialCellSet(p).internalGates||[]){if(pair?.a===index)return pair.b;if(pair?.b===index)return pair.a}return null}
function invalidateSpecialCellCaches(p){if(p){delete p._warpMap;delete p._internalGateIndexes;delete p._gateCells}}
function reservedSpecialKeys(p){
const keys=new Set(),special=specialCellSet(p);
for(const cell of special.crossings)keys.add(ckey(...cell));
for(const pair of special.warps){keys.add(ckey(...pair.a));keys.add(ckey(...pair.b))}
for(const lock of special.locks){keys.add(ckey(...lock.key));keys.add(ckey(...lock.door))}
for(const pair of special.internalGates){const a=p.g?.[pair.a],b=p.g?.[pair.b];if(a)keys.add(ckey(a[0],a[1]));if(b)keys.add(ckey(b[0],b[1]))}
return keys;
}
function rebuildSolutionClues(p,seed){
const reserved=reservedSpecialKeys(p),rng=rngFrom(hash32(seed^0x3c6ef372));p.n=[];let maxTurns=0,totalTurns=0;
for(const path of p.solution){
const analysis=turnAnalysis(path,p);if(!analysis.count||!analysis.cells.length)return false;
const pool=analysis.cells.filter(cell=>!reserved.has(ckey(...cell))),candidates=pool.length?pool:analysis.cells,
clue=candidates[Math.floor(rng()*candidates.length)];
p.n.push([clue[0],clue[1],analysis.count]);maxTurns=Math.max(maxTurns,analysis.count);totalTurns+=analysis.count;
}
p.maxTurns=maxTurns;p.totalTurns=totalTurns;return true;
}
function addWarpSpecial(p,rng,reserved){
const candidates=[];
p.solution.forEach((path,pathIndex)=>{
const cells=path.cells;
for(let i=1;i<cells.length-5;i++)for(let j=i+3;j<cells.length-2;j++){
const a=cells[i],b=cells[j],afterA=cells[i+1],afterB=cells[j+1];
if(reserved.has(ckey(...a))||reserved.has(ckey(...b))||manhattan(a,b)<=2||manhattan(afterA,afterB)!==1)continue;
candidates.push({pathIndex,i,j,a:[...a],b:[...b],distance:manhattan(a,b)});
}
});
if(!candidates.length)return false;
candidates.sort((a,b)=>b.distance-a.distance||rng()-.5);const chosen=candidates[Math.floor(rng()*Math.min(8,candidates.length))],path=p.solution[chosen.pathIndex],cells=path.cells;
path.cells=[...cells.slice(0,chosen.i+1),...cells.slice(chosen.i+1,chosen.j+1).reverse(),...cells.slice(chosen.j+1)];
specialCellSet(p).warps.push({a:chosen.a,b:chosen.b});invalidateSpecialCellCaches(p);reserved.add(ckey(...chosen.a));reserved.add(ckey(...chosen.b));return true;
}
function addLockSpecial(p,rng,reserved){
const candidates=[];
p.solution.forEach((path,pathIndex)=>{
const cells=path.cells;
for(let keyIndex=1;keyIndex<cells.length-5;keyIndex++)for(let doorIndex=keyIndex+3;doorIndex<cells.length-1;doorIndex++){
const keyCell=cells[keyIndex],door=cells[doorIndex];
if(reserved.has(ckey(...keyCell))||reserved.has(ckey(...door)))continue;
candidates.push({pathIndex,keyIndex,doorIndex,key:[...keyCell],door:[...door],span:doorIndex-keyIndex});
}
});
if(!candidates.length)return false;
candidates.sort((a,b)=>b.span-a.span||rng()-.5);const chosen=candidates[Math.floor(rng()*Math.min(10,candidates.length))];
specialCellSet(p).locks.push({key:chosen.key,door:chosen.door});reserved.add(ckey(...chosen.key));reserved.add(ckey(...chosen.door));return true;
}
function buildCrossingTemplate(p,reserved){
if(reserved.size)return false;
const allCells=[...(p.valid||[]),...(p.obstacles||[])],chunkSet=new Set(allCells.map(([r,c])=>key2(Math.floor(c/5),Math.floor(r/5)))),chunks=[...chunkSet].map(key=>key.split(',').map(Number));
const directions=[['N',0,-1],['E',1,0],['S',0,1],['W',-1,0]],rotateSide=(side,turns)=>directions[(directions.findIndex(item=>item[0]===side)+turns)%4][0],rotateCell=([r,c],turns)=>{let row=r,col=c;for(let i=0;i<turns;i++)[row,col]=[col,4-row];return[row,col]};
let chosen=null;
for(const[chunkX,chunkY]of chunks){
const external=new Set(directions.filter(([,dx,dy])=>!chunkSet.has(key2(chunkX+dx,chunkY+dy))).map(item=>item[0]));
for(let turns=0;turns<4;turns++)if(external.has(rotateSide('N',turns))&&external.has(rotateSide('W',turns))){chosen={chunkX,chunkY,turns};break}
if(chosen)break;
}
if(!chosen)return false;
const basePaths=[
{startGate:0,endGate:1,openGate:null,cells:[[2,0],[1,0],[1,1],[2,1],[2,2],[2,3],[3,3],[4,3],[4,4],[3,4],[2,4],[1,4],[1,3],[0,3]]},
{startGate:2,endGate:3,openGate:null,cells:[[0,1],[0,2],[1,2],[2,2],[3,2],[4,2],[4,1],[4,0]]}
],baseGates=[[2,0,'W'],[0,3,'N'],[0,1,'N'],[4,0,'W']],offsetCell=cell=>{const[row,col]=rotateCell(cell,chosen.turns);return[chosen.chunkY*5+row,chosen.chunkX*5+col]};
p.solution=basePaths.map(path=>({...path,cells:path.cells.map(offsetCell)}));
p.g=baseGates.map(([r,c,side])=>{const[row,col]=offsetCell([r,c]);return[row,col,rotateSide(side,chosen.turns)]});
const crossing=offsetCell([2,2]),validKeys=new Set();for(const path of p.solution)for(const cell of path.cells)validKeys.add(ckey(...cell));
const nextValid=allCells.filter(cell=>validKeys.has(ckey(...cell))),nextObstacles=allCells.filter(cell=>!validKeys.has(ckey(...cell)));
if(nextObstacles.length>obstacleCellLimit(p))return false;
p.valid=nextValid;p.obstacles=nextObstacles;p.n=[];p.specialCells={crossings:[crossing],warps:[],locks:[],internalGates:[]};invalidateSpecialCellCaches(p);reserved.add(ckey(...crossing));return true;
}
function addCrossingSpecial(p,rng,reserved){
const valid=cellSet(p),gateCells=new Set(p.g.map(g=>ckey(g[0],g[1]))),candidates=[];
p.solution.forEach((basePath,basePathIndex)=>{
for(let baseIndex=1;baseIndex<basePath.cells.length-1;baseIndex++){
const cell=basePath.cells[baseIndex],previous=basePath.cells[baseIndex-1],next=basePath.cells[baseIndex+1],key=ckey(...cell);
if(reserved.has(key)||gateCells.has(key))continue;
const north=[cell[0]-1,cell[1]],south=[cell[0]+1,cell[1]],west=[cell[0],cell[1]-1],east=[cell[0],cell[1]+1],neighbors=[north,south,west,east];
if(neighbors.some(candidate=>!valid.has(ckey(...candidate))))continue;
const baseAxis=previous[0]===cell[0]&&next[0]===cell[0]?'H':previous[1]===cell[1]&&next[1]===cell[1]?'V':null;
if(!baseAxis)continue;
const [entry,exit]=baseAxis==='H'?[north,south]:[west,east];
p.solution.forEach((crossPath,crossPathIndex)=>{
if(crossPathIndex===basePathIndex||crossPath.cells.some(candidate=>sameCell(candidate,cell)))return;
const entryIndex=crossPath.cells.findIndex(candidate=>sameCell(candidate,entry)),exitIndex=crossPath.cells.findIndex(candidate=>sameCell(candidate,exit));
if(entryIndex<0||exitIndex<0||Math.abs(entryIndex-exitIndex)<2)return;
const lo=Math.min(entryIndex,exitIndex),hi=Math.max(entryIndex,exitIndex),removed=crossPath.cells.slice(lo+1,hi);
if(!removed.length||(p.obstacles?.length||0)+removed.length>obstacleCellLimit(p)||removed.some(candidate=>sameCell(candidate,cell)||reserved.has(ckey(...candidate))||gateCells.has(ckey(...candidate))))return;
for(let index=lo+1;index<=hi;index++)if(isWarpTransition(p,crossPath.cells[index-1],crossPath.cells[index]))return;
candidates.push({cell:[...cell],basePathIndex,crossPathIndex,lo,hi,removed:removed.map(candidate=>[...candidate]),score:removed.length+Math.min(baseIndex,basePath.cells.length-1-baseIndex)});
});
}
});
if(!candidates.length)return buildCrossingTemplate(p,reserved);
candidates.sort((a,b)=>a.removed.length-b.removed.length||b.score-a.score||rng()-.5);const chosen=candidates[Math.floor(rng()*Math.min(12,candidates.length))],crossPath=p.solution[chosen.crossPathIndex];
crossPath.cells=[...crossPath.cells.slice(0,chosen.lo+1),[...chosen.cell],...crossPath.cells.slice(chosen.hi)];
const removedKeys=new Set(chosen.removed.map(cell=>ckey(...cell)));
p.valid=p.valid.filter(cell=>!removedKeys.has(ckey(...cell)));p.obstacles=[...(p.obstacles||[]),...chosen.removed];
specialCellSet(p).crossings.push(chosen.cell);invalidateSpecialCellCaches(p);reserved.add(ckey(...chosen.cell));
return Boolean(crossingStateAtCell({paths:p.solution},p,chosen.cell));
}
function addInternalGateSpecial(p,rng,reserved){
const valid=cellSet(p),gateCells=new Set(p.g.map(g=>ckey(g[0],g[1]))),candidates=[],sideFor=(a,b)=>b[0]<a[0]?'N':b[0]>a[0]?'S':b[1]<a[1]?'W':'E';
p.solution.forEach((path,pathIndex)=>{
for(let cut=1;cut<path.cells.length-2;cut++){
const a=path.cells[cut],b=path.cells[cut+1],ak=ckey(...a),bk=ckey(...b);if(manhattan(a,b)!==1||reserved.has(ak)||reserved.has(bk)||gateCells.has(ak)||gateCells.has(bk))continue;
const separatesLock=specialCellSet(p).locks.some(lock=>{const keyIndex=path.cells.findIndex(cell=>sameCell(cell,lock.key)),doorIndex=path.cells.findIndex(cell=>sameCell(cell,lock.door));return keyIndex>=0&&doorIndex>=0&&(keyIndex<=cut)!==(doorIndex<=cut)});if(separatesLock)continue;
// Internal gates are rendered at cell centers, so even a cell on the outer row/column remains an in-board gate rather than a world-edge gate.
if(!valid.has(ak)||!valid.has(bk))continue;
candidates.push({pathIndex,cut,a:[...a],b:[...b],balance:Math.min(cut,path.cells.length-cut-2)});
}
});
if(!candidates.length)return false;
candidates.sort((a,b)=>b.balance-a.balance||rng()-.5);
const preferred=candidates.splice(Math.floor(rng()*Math.min(10,candidates.length)),1)[0],ordered=[preferred,...candidates];
for(const chosen of ordered){
const path=p.solution[chosen.pathIndex],aIndex=p.g.length,bIndex=p.g.length+1,sideA=sideFor(chosen.a,chosen.b),sideB=OPP[sideA],
prefix={...path,endGate:aIndex,cells:path.cells.slice(0,chosen.cut+1)},suffix={...path,startGate:bIndex,cells:path.cells.slice(chosen.cut+1)},special=specialCellSet(p);
p.g.push([chosen.a[0],chosen.a[1],sideA],[chosen.b[0],chosen.b[1],sideB]);p.solution.splice(chosen.pathIndex,1,prefix,suffix);special.internalGates.push({a:aIndex,b:bIndex});invalidateSpecialCellCaches(p);
const clueable=[prefix,suffix].every(segment=>{const analysis=turnAnalysis(segment,p);return analysis.count>0&&analysis.cells.length>0});
if(clueable){reserved.add(ckey(...chosen.a));reserved.add(ckey(...chosen.b));return true}
special.internalGates.pop();p.solution.splice(chosen.pathIndex,2,path);p.g.length=aIndex;invalidateSpecialCellCaches(p);
}
return false;
}
function recentSpecialMechanicTypes(limit=3){
const result=[],metas=Object.values(data.metas||{}).sort((left,right)=>(Number(right.id?.slice(1))||0)-(Number(left.id?.slice(1))||0));
for(const meta of metas)for(const type of mechanicTypesForPuzzle(meta.puzzle)){result.push(type);if(result.length>=limit)return result.reverse()}
return result.reverse();
}
function addScheduledSpecial(puzzle,type,rng,reserved){
if(type==='warp')return addWarpSpecial(puzzle,rng,reserved);
if(type==='lock')return addLockSpecial(puzzle,rng,reserved);
if(type==='crossing')return addCrossingSpecial(puzzle,rng,reserved);
if(type==='internalGate')return addInternalGateSpecial(puzzle,rng,reserved);
return false;
}
function specialCellUsage(puzzle){
const special=specialCellSet(puzzle);
return(special.crossings?.length||0)+(special.warps?.length||0)*2+(special.locks?.length||0)*2+(special.internalGates?.length||0)*2;
}
function addSpecialCellPattern(sourcePuzzle,seed,level){
const scheduled=AppLogic.specialSchedule(level,sourcePuzzle.valid?.length||0,seed,data.specialMechanicsSeen||[],recentSpecialMechanicTypes()),
requiredTypes=[...scheduled.types];
if(!requiredTypes.length)return sourcePuzzle;
const schedule={...scheduled,types:requiredTypes,setCount:Math.max(scheduled.setCount||0,requiredTypes.length),maxCells:Math.max(scheduled.maxCells||0,requiredTypes.reduce((sum,type)=>sum+(type==='crossing'?1:2),0))},
puzzle=deepClone(sourcePuzzle),special=specialCellSet(puzzle),reserved=reservedSpecialKeys(puzzle);
special.crossings=[];special.warps=[];special.locks=[];special.internalGates=[];invalidateSpecialCellCaches(puzzle);reserved.clear();
const plan=[...schedule.types];for(let index=plan.length;index<schedule.setCount;index++)plan.push(schedule.types[index%schedule.types.length]);
let added=0;
for(let index=0;index<plan.length;index++){
const type=plan[index],cellCost=type==='crossing'?1:2;
if(specialCellUsage(puzzle)+cellCost>schedule.maxCells){if(index<schedule.types.length)return null;continue}
const rng=rngFrom(hash32(seed^AppLogic.stableHash(['scheduled-special',type,index])));
if(addScheduledSpecial(puzzle,type,rng,reserved))added++;
else if(index<schedule.types.length)return null;
}
if(added<schedule.types.length)return null;
if(!rebuildSolutionClues(puzzle,seed))return null;
if(special.crossings.length&&!isSolved({paths:deepClone(puzzle.solution),specialProgress:{crossings:[]}},puzzle))return null;
puzzle.specialSchedule={version:2,types:[...schedule.types],setCount:added,introduction:schedule.introduction===true};
puzzle.style=`${puzzle.style||'procedural'}-special`;return puzzle;
}
function installPreparedChild(prepared){
if(!prepared||prepared.futureBoardNumber!==data.nextId)return null;
const source=data.metas[prepared.sourceId],frontier=prepared.frontier,puzzle=prepared.puzzle,shape=prepared.chunks,x=prepared.x,y=prepared.y;
const terminalFill=frontier?.terminalFill===true&&isClosedVoidUnit(frontier.unitX,frontier.unitY);
if(!source||!frontier||!puzzle||occupancy.has(key2(frontier.unitX,frontier.unitY))||isClosedVoidUnit(frontier.unitX,frontier.unitY)&&!terminalFill)return null;
const targetRange=sectionCountRange(prepared.targetLevel);
if(!fitsMeta(x,y,shape)||!placementPreservesUnsolvedFrontiers(x,y,shape,source.id,{allowTerminal:terminalFill})||targetLevelForNewBoard(x,y,shape,data.nextId)!==prepared.targetLevel||
!terminalFill&&(shape.length<targetRange.min||shape.length>targetRange.max))return null;
const connectionRequirements=placementConnectionRequirements(x,y,shape),required=new Set((frontier.contacts||[]).map(contact=>contact.gateIndex).filter(Number.isInteger));
if(frontier.gateDriven&&[...required].some(gateIndex=>!connectionRequirements.some(requirement=>requirement.metaId===source.id&&requirement.gateIndex===gateIndex)))return null;
if(!puzzleSupportsConnectionRequirements(puzzle,x,y,connectionRequirements))return null;
const id='B'+data.nextId++,meta={id,x,y,chunks:shape,level:prepared.level,targetLevel:prepared.targetLevel,seed:prepared.generationSeed,axis:puzzle.axis,entrySide:prepared.targetSide,sealedSides:[...prepared.sealedSides],puzzle,justRevealed:true,generatorVersion:GENERATOR_VERSION,rev:nextRevision()};
data.metas[id]=meta;
const introduced=mechanicTypesForPuzzle(puzzle),seen=new Set(data.specialMechanicsSeen||[]);let encounterChanged=false;
for(const type of introduced)if(!seen.has(type)){seen.add(type);encounterChanged=true}
if(encounterChanged)data.specialMechanicsSeen=[...seen].sort();
markMetaDirty(id);ensureMetaState(id,{dirty:false});markStateDirty(id);addMetaToOccupancy(meta);return meta;
}
function nearbyShapeFamilyCounts(unitX,unitY,radius=7){
const ids=new Set(),counts=new Map();
for(let y=unitY-radius;y<=unitY+radius;y++)for(let x=unitX-radius;x<=unitX+radius;x++){const id=occupancy.get(key2(x,y));if(id)ids.add(id)}
for(const id of ids){const chunks=data.metas[id]?.chunks;if(!chunks?.length)continue;const family=generatedShapeFamilyKey(chunks);counts.set(family,(counts.get(family)||0)+1)}
return counts;
}
function shapeCandidatesForArea(candidates,seed,unitX,unitY){
const balanced=balancedShapeCandidates(candidates,seed,{hash32,rngFrom,shuffle}),nearby=nearbyShapeFamilyCounts(unitX,unitY);
return balanced.map((shape,index)=>({shape,index,count:nearby.get(generatedShapeFamilyKey(shape))||0})).sort((left,right)=>left.count-right.count||left.index-right.index).map(entry=>entry.shape);
}
async function placeChildAtFrontierAttempt(source,frontier,attemptBase=0,{prepareOnly=false,futureBoardNumber=data.nextId}={}){
const targetUnitX=frontier.unitX,targetUnitY=frontier.unitY;
const terminalFill=frontier.terminalFill===true&&isClosedVoidUnit(targetUnitX,targetUnitY);
if(occupancy.has(key2(targetUnitX,targetUnitY))||isClosedVoidUnit(targetUnitX,targetUnitY)&&!terminalFill)return null;
const targetSide=frontier.targetSide,preliminaryLevel=macroDifficulty(targetUnitX,targetUnitY),
candidateSeed=hash32(source.seed^Math.imul(targetUnitX+101,49999)^Math.imul(targetUnitY-211,31337)^attemptBase);
const tryShape=async(shape,seed,rng,{allowRegionalFallback=false}={})=>{
const alignments=shuffle([...shape],rng);
for(const[dx,dy]of alignments){
const x=targetUnitX-dx,y=targetUnitY-dy,placementIsTerminal=terminalFill&&shape.length===1&&x===targetUnitX&&y===targetUnitY;
if(!fitsMeta(x,y,shape)||!placementPreservesUnsolvedFrontiers(x,y,shape,source.id,{allowTerminal:placementIsTerminal}))continue;
const targetLevel=targetLevelForNewBoard(x,y,shape,futureBoardNumber),actualRange=sectionCountRange(targetLevel);
if(!placementIsTerminal&&(shape.length<actualRange.min||shape.length>actualRange.max))continue;
const connectionRequirements=placementConnectionRequirements(x,y,shape),requiredFrontierGates=new Set((frontier.contacts||[]).map(contact=>contact.gateIndex).filter(Number.isInteger));
if(frontier.gateDriven&&[...requiredFrontierGates].some(gateIndex=>!connectionRequirements.some(requirement=>requirement.metaId===source.id&&requirement.gateIndex===gateIndex)))continue;
const fixedPortProfiles=fixedPortProfilesForRequirements(connectionRequirements);if(fixedPortProfiles==null)continue;
let accepted=null,lastGenerationIssue=null;
const generationAttempts=shape.length===1?5:4;
for(let variant=0;variant<generationAttempts;variant++){
const generationSeed=variant===0?seed:hash32(seed^Math.imul(variant+1,0x7f4a7c15)),
portSeed=variant===0?null:hash32(generationSeed^0x243f6a88),obstacleSeed=hash32(generationSeed^0xb7e15162),specialSeed=hash32(generationSeed^0x9e3779b9),
generationOptions=portSeed==null?{fixedPortProfiles}:{portSeed,fixedPortProfiles};
let generated;
try{generated=await generatePuzzleAsync(shape,generationSeed,targetLevel,x,y,shape.length>=7?9000:4000,generationOptions)}
catch(error){lastGenerationIssue=error?.message||String(error);continue}
let puzzle=addObstaclePattern(generated,obstacleSeed);puzzle=addSpecialCellPattern(puzzle,specialSeed,targetLevel);
if(!puzzle){lastGenerationIssue='\u7279\u6b8a\u30bb\u30eb\u5272\u308a\u5f53\u3066\u4e0d\u6210\u7acb';continue}
let issue=generatedPuzzleIssue(puzzle);
if(!issue&&!puzzleSupportsConnectionRequirements(puzzle,x,y,connectionRequirements))issue='\u65e2\u5b58\u30b2\u30fc\u30c8\u3068\u4e0d\u4e00\u81f4';
if(!issue&&!placementIsTerminal&&(shape.length<actualRange.min||shape.length>actualRange.max))issue='\u30ec\u30d9\u30eb\u5225\u306e\u533a\u753b\u6570\u7bc4\u56f2\u5916';
const intrinsicLevel=solverDifficulty(puzzle,targetLevel),intrinsicInBand=difficultyFitsRegion(intrinsicLevel,targetLevel);
const level=intrinsicLevel,inBand=intrinsicInBand;
puzzle.difficulty=level;puzzle.level=level;puzzle.regionalTarget=targetLevel;
if(!issue&&level<=3&&shape.length!==1)issue='low-level board must use one section';
if(!issue&&!inBand&&!allowRegionalFallback)issue='\u5730\u57df\u96e3\u6613\u5ea6\u7bc4\u56f2\u5916';
const burden=AppLogic.interactionBurden(puzzle);puzzle.interactionBurden=burden;
if(!issue&&burden.score>=39)issue='\u64cd\u4f5c\u8ca0\u62c5\u304c\u4e0a\u9650\u3092\u8d85\u904e';
if(!issue&&level>=UNIQUE_SOLUTION_MIN_LEVEL){
let verification;try{verification=await verifyPuzzleUniquenessAsync(puzzle)}catch(error){lastGenerationIssue=error?.message||String(error);continue}
if(verification.status!=='unique')issue=`\u4e00\u610f\u89e3\u691c\u8a3c\uff1a${verification.status}`;
else{
const boundedBurdenPenalty=Math.min(14,burden.score*.35),qualityScore=Math.max(0,(verification.quality?.score||0)-boundedBurdenPenalty);
puzzle.uniqueness={status:'unique',signature:verification.signature,ruleVersion:verification.ruleVersion,nodes:verification.nodes};
puzzle.solutionQuality={...verification.quality,interactionBurdenPenalty:boundedBurdenPenalty,score:qualityScore,accepted:verification.quality?.accepted===true&&qualityScore>=10};
if(!puzzle.solutionQuality.accepted)issue='\u89e3\u6cd5\u54c1\u8cea\u30b9\u30b3\u30a2\u4e0d\u8db3';
}
}
if(issue){lastGenerationIssue=issue;continue}
accepted={puzzle,generationSeed,level,inBand,portSeed,obstacleSeed,specialSeed};break;
}
if(!accepted){if(globalThis.BEND_DEBUG_GENERATION&&lastGenerationIssue)console.debug('BEND FIELD: regenerated rejected board candidate',lastGenerationIssue);continue}
const{puzzle,generationSeed,level,inBand}=accepted;
if(!fitsMeta(x,y,shape)||!placementPreservesUnsolvedFrontiers(x,y,shape,source.id,{allowTerminal:placementIsTerminal})||occupancy.has(key2(targetUnitX,targetUnitY))||isClosedVoidUnit(targetUnitX,targetUnitY)&&!placementIsTerminal)continue;
if(!inBand)puzzle.regionalFallback=true;
const requiredOpenSides=new Set([...(frontier.gateDriven||!placementIsTerminal?[targetSide]:[]),...connectionRequirements.map(requirement=>requirement.targetSide)]);
const sealedSides=(placementIsTerminal?['N','S','W','E']:[]).filter(side=>!requiredOpenSides.has(side)),
prepared={sourceId:source.id,frontier:deepClone(frontier),futureBoardNumber,x,y,chunks:shape.map(cell=>[...cell]),level,targetLevel,generationSeed,targetSide,sealedSides,puzzle};
return prepareOnly?prepared:installPreparedChild(prepared);
}
return null;
};
const candidateLevels=new Set([preliminaryLevel,macroDifficulty(targetUnitX,targetUnitY)]);
const candidates=[],candidateKeys=new Set();
for(const levelCandidate of candidateLevels)for(const shape of shapeCandidatesForLevel(hash32(candidateSeed^Math.imul(levelCandidate,0x45d9f3b)),levelCandidate,12)){const key=generatedShapeKey(shape);if(!candidateKeys.has(key)){candidateKeys.add(key);candidates.push(shape)}}
for(const shape of shuffle([...SHAPES],rngFrom(hash32(candidateSeed^0x3c6ef372)))){const key=generatedShapeKey(shape);if(!candidateKeys.has(key)){candidateKeys.add(key);candidates.push(shape)}}
const orderedCandidates=shapeCandidatesForArea(candidates,hash32(candidateSeed^0xbb67ae85),targetUnitX,targetUnitY);
for(let pass=0;pass<2;pass++)for(let index=0;index<orderedCandidates.length;index++){
const shape=orderedCandidates[index],seed=hash32(candidateSeed^Math.imul(index+1,0x9e3779b9)^Math.imul(pass+1,0x45d9f3b)),made=await tryShape(shape,seed,rngFrom(seed),{allowRegionalFallback:pass===1});
if(made)return made;
}
// Safe final fallback: a single 5x5 section at the exact gate frontier.
// It is accepted only when every existing facing gate, occupancy rule, and
// future-frontier constraint is still satisfied; no board is forced into place.
const fallbackShape=[[0,0]];
for(let attempt=0;attempt<4;attempt++){
const seed=hash32(candidateSeed^0x6a09e667^Math.imul(attempt+1,0x85ebca6b)),made=await tryShape(fallbackShape,seed,rngFrom(seed),{allowRegionalFallback:true});
if(made)return made;
}
return null;
}
function frontierGeometryStillViable(source,frontier){
if(!source||data.metas[source.id]!==source)return false;
const targetKey=key2(frontier.unitX,frontier.unitY),terminalFill=frontier.terminalFill===true&&isClosedVoidUnit(frontier.unitX,frontier.unitY);
if(occupancy.has(targetKey)||isClosedVoidUnit(frontier.unitX,frontier.unitY)&&!terminalFill)return false;
const shapes=[...SHAPES,[[0,0]]],seen=new Set();
for(const shape of shapes){
const shapeKey=generatedShapeKey(shape);if(seen.has(shapeKey))continue;seen.add(shapeKey);
for(const[dx,dy]of shape){
const x=frontier.unitX-dx,y=frontier.unitY-dy;
const placementIsTerminal=terminalFill&&shape.length===1&&x===frontier.unitX&&y===frontier.unitY;
if(!fitsMeta(x,y,shape)||!placementPreservesUnsolvedFrontiers(x,y,shape,source.id,{allowTerminal:placementIsTerminal}))continue;
const range=sectionCountRange(targetLevelForNewBoard(x,y,shape,data.nextId));if(!placementIsTerminal&&(shape.length<range.min||shape.length>range.max))continue;
const requirements=placementConnectionRequirements(x,y,shape),required=new Set((frontier.contacts||[]).map(contact=>contact.gateIndex).filter(Number.isInteger));
if(frontier.gateDriven&&[...required].some(gateIndex=>!requirements.some(requirement=>requirement.metaId===source.id&&requirement.gateIndex===gateIndex)))continue;
if(fixedPortProfilesForRequirements(requirements)!=null)return true;
}
}
return false;
}
async function placeChildAtFrontier(source,frontier,attemptBase=0,options=null){
let cycle=0;
while(cycle<MAX_FRONTIER_GENERATION_CYCLES&&frontierGeometryStillViable(source,frontier)){
const cycleBase=attemptBase+cycle*2000003,made=await placeChildAtFrontierAttempt(source,frontier,cycleBase,options||undefined);
if(made)return made;
cycle++;
if(cycle%2===0){if(globalThis.BEND_DEBUG_GENERATION)console.debug('BEND FIELD: retrying full board generation',source.id,frontier.unitX,frontier.unitY,cycle);await sleep(0)}
}
return null;
}
function unsolvedBoardCount(){let count=0;for(const id of Object.keys(data.metas))if(!metaState(id).solved)count++;return count}
async function ensureUnsolvedFrontier(preferredMeta=null){
if(unsolvedBoardCount()>0)return 0;
rebuildOccupancy();
const metas=[];if(preferredMeta)metas.push(preferredMeta);
for(const meta of Object.values(data.metas))if(meta!==preferredMeta&&metaState(meta.id).solved)metas.push(meta);
metas.sort((a,b)=>(a===preferredMeta?-1:b===preferredMeta?1:0)||(b.rev||0)-(a.rev||0));
for(const meta of metas){
if(!meta.puzzle){try{await hydrateMeta(meta)}catch(_){continue}}
await hydrateAdjacentMetas(meta);
const gateCandidates=gateFrontierCandidates(meta),candidates=gateCandidates.length?gateCandidates:frontierCandidates(meta);
for(let offset=0;offset<candidates.length;offset++)if(await placeChildAtFrontier(meta,candidates[offset],250000+offset*1000))return 1;
}
return 0;
}
function canExpandSharedBoard(meta,state=meta?.id?metaState(meta.id):null){return Boolean(meta&&state?.solved&&(!cloudAvailable||!data.cloudProfile||state.solvedById===currentPlayerId()))}
async function prepareExpansionCandidate(meta){
if(!meta||data.metas[meta.id]!==meta||!canExpandSharedBoard(meta))return null;
const boardNumber=data.nextId,frontiers=unresolvedExpansionCandidates(meta);
for(let index=0;index<frontiers.length;index++){
const prepared=await placeChildAtFrontierAttempt(meta,frontiers[index],index*1000,{prepareOnly:true,futureBoardNumber:boardNumber});
if(prepared)return prepared;
}
return null;
}
async function expandMetaNow(meta,prepared=null){
const st=metaState(meta.id);if(!canExpandSharedBoard(meta,st)||st.expanded)return 0;
await hydrateAdjacentMetas(meta);rebuildOccupancy();repairFacingGateConnections(meta);
st.expanded=false;
const retryRound=Math.max(0,Number.isInteger(st.expansionRetryRound)?st.expansionRetryRound:0);
let made=0,attemptOffset=retryRound*37000017;
if(prepared&&installPreparedChild(prepared))made++;
const placeTargets=async frontiers=>{for(const frontier of frontiers){if(await placeChildAtFrontier(meta,frontier,attemptOffset))made++;attemptOffset+=1000}};
await placeTargets(unresolvedExpansionCandidates(meta));
for(const retryBase of[500000,900000,1400000]){const remaining=unresolvedExpansionCandidates(meta);if(!remaining.length)break;attemptOffset=Math.max(attemptOffset,retryBase);await placeTargets(remaining)}
if(unsolvedBoardCount()===0)made+=await ensureUnsolvedFrontier(meta);
const missingConnections=missingGateConnections(meta);
st.expanded=missingConnections.length===0;st.expansionRetryRound=st.expanded?0:Math.min(1000000,retryRound+1);st.rev=nextRevision();markStateDirty(meta.id);
const saved=await refreshWorldView({rebuild:false,syncConnections:true,hide:true,persist:true,immediate:true,lockHeld:true});await nextPaint();
if(!saved)showStatus('\u76e4\u9762\u306f\u751f\u6210\u6e08\u307f\u3067\u3059\u3002\u4fdd\u5b58\u3092\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002',{retry:true,fresh:false});
if(unsolvedBoardCount()===0)st.expanded=false;
return made;
}
function expandMeta(meta,prepared=null){
const id=meta?.id;
return enqueueWorldMutation(async()=>{
const current=data.metas[id];if(!current)return 0;
if(!current.puzzle)await hydrateMeta(current);return expandMetaNow(current,prepared);
});
}
async function repairMetaFrontierNow(meta){
const st=metaState(meta.id);if(!canExpandSharedBoard(meta,st)||st.expanded)return 0;
if(!meta.puzzle)await hydrateMeta(meta);await hydrateAdjacentMetas(meta);rebuildOccupancy();repairFacingGateConnections(meta);
const frontiers=unresolvedExpansionCandidates(meta),round=Math.max(0,Number.isInteger(st.expansionRetryRound)?st.expansionRetryRound:0);
let made=0;
if(frontiers.length){
const frontier=frontiers[round%frontiers.length],attemptBase=310000003+Math.imul(round+1,1000003);
if(await placeChildAtFrontierAttempt(meta,frontier,attemptBase))made=1;
}
const missing=missingGateConnections(meta);st.expanded=missing.length===0;
st.expansionRetryRound=st.expanded?0:Math.min(1000000,round+1);st.rev=nextRevision();markStateDirty(meta.id);return made;
}
function repairMetaFrontier(meta){
const id=meta?.id;
return enqueueWorldMutation(async()=>{const current=data.metas[id];return current?repairMetaFrontierNow(current):0});
}
function sharedExpansionRepairDelay(state){return state?.solved&&state.solvedById===currentPlayerId()?0:Infinity}
function nextSharedExpansionRepairDelay(){return Object.values(data.metas).some(meta=>canExpandSharedBoard(meta)&&!metaState(meta.id).expanded)?0:null}
function hasSharedExpansionRepairAuthority(){return Object.values(data.metas).some(meta=>canExpandSharedBoard(meta)&&!metaState(meta.id).expanded)}
async function repairExpansions(){
if(typeof fieldArchiveBusy!=='undefined'&&fieldArchiveBusy){scheduleExpansionRepair(1000);return 0}
let made=0;
for(let pass=0;pass<3;pass++){
let passMade=0;const active=data.metas[activeBoard],pending=Object.values(data.metas).filter(meta=>{const st=metaState(meta.id);return canExpandSharedBoard(meta,st)&&!st.expanded});
pending.sort((left,right)=>{
const leftDistance=active?Math.hypot(left.x-active.x,left.y-active.y):0,rightDistance=active?Math.hypot(right.x-active.x,right.y-active.y):0;
return leftDistance-rightDistance||(metaState(left.id).expansionRetryRound||0)-(metaState(right.id).expansionRetryRound||0)||left.id.localeCompare(right.id,undefined,{numeric:true});
});
for(const meta of pending)passMade+=await repairMetaFrontier(meta)||0;
made+=passMade;
}
let closedOffset=0;
for(const{source,frontier}of closedVoidRepairCandidates().filter(({source})=>canExpandSharedBoard(source)).slice(0,2)){
if(await placeChildAtFrontierAttempt(source,frontier,470000003+closedOffset*1000003))made++;
closedOffset++;
}
if(unsolvedBoardCount()===0&&hasSharedExpansionRepairAuthority())made+=await ensureUnsolvedFrontier(data.metas.B0||Object.values(data.metas)[0]);
await refreshWorldView({rebuild:true,syncConnections:true,hide:true,persist:true,immediate:true});if(pendingExpansionCount())scheduleExpansionRepair(700);return made;
}
let expansionRepairTimer=0;
function scheduleExpansionRepair(delay=700){if(expansionRepairTimer)return;const sharedWait=nextSharedExpansionRepairDelay();if(sharedWait==null&&!pendingExpansionCount())return;expansionRepairTimer=setTimeout(()=>{expansionRepairTimer=0;void repairExpansions().catch(handleExpansionError)},Math.max(150,delay,sharedWait||0))}
function reopenMissingGateExpansions(){
rebuildOccupancy();let reopened=0;
for(const meta of Object.values(data.metas)){
const st=metaState(meta.id);if(!canExpandSharedBoard(meta,st)||!meta.puzzle)continue;
repairFacingGateConnections(meta);
if(st.expanded&&missingGateConnections(meta).length){st.expanded=false;st.rev=nextRevision();markStateDirty(meta.id);reopened++}
}
return reopened;
}
function pendingExpansionCount(){
let count=0;
for(const meta of Object.values(data.metas)){const st=metaState(meta.id);if(canExpandSharedBoard(meta,st)&&!st.expanded)count++}
return count+closedVoidRepairCandidates().filter(({source})=>canExpandSharedBoard(source)).length;
}
function cellSet(p){return p._validSet||(p._validSet=new Set(p.valid.map(c=>ckey(...c))))}
function warpMap(p){
if(p._warpMap)return p._warpMap;const map=new Map();
for(const pair of specialCellSet(p).warps){map.set(ckey(...pair.a),pair.b);map.set(ckey(...pair.b),pair.a)}
return p._warpMap=map;
}
function warpPairForCell(p,cell){return cell?warpMap(p).get(ckey(...cell))||null:null}
function isWarpTransition(p,a,b){const pair=warpPairForCell(p,a);return!!pair&&sameCell(pair,b)}
function pathCellsAdjacent(p,a,b){return manhattan(a,b)===1||isWarpTransition(p,a,b)}
function lockForDoor(p,cell){return specialCellSet(p).locks.find(lock=>sameCell(lock.door,cell))||null}
function pathHasLockKey(path,lock){return!!lock&&path.cells.some(cell=>sameCell(cell,lock.key))}
function crossingKeys(p){return specialCellSet(p).crossings.map(cell=>ckey(...cell))}
function crossingsSatisfied(st,p){return crossingKeys(p).every(key=>Boolean(crossingStateAtCell(st,p,key)))}
function pathIndexesAtCell(st,cellOrKey){
const key=typeof cellOrKey==='string'?cellOrKey:ckey(...cellOrKey),indexes=[];
st.paths.forEach((path,index)=>{if(path?.cells?.some(cell=>ckey(...cell)===key))indexes.push(index)});return indexes;
}
function crossingStateAtCell(st,p,cellOrKey){
const key=typeof cellOrKey==='string'?cellOrKey:ckey(...cellOrKey);if(!crossingKeys(p).includes(key))return null;
const cell=key.split(',').map(Number),entries=[];
st.paths.forEach((path,index)=>{if(!path?.cells?.some(candidate=>sameCell(candidate,cell)))return;const axis=pathAxisAtCell(path,p,cell);if(axis)entries.push({index,axis})});
if(entries.length!==2||entries[0].axis===entries[1].axis)return null;
return{key,cell,horizontal:entries.find(entry=>entry.axis==='H')?.index,vertical:entries.find(entry=>entry.axis==='V')?.index};
}
function crossingExitDirection(st,p,pathIndex){
const path=st.paths[pathIndex];if(!path?.cells||path.cells.length<2)return null;const cell=path.cells[path.cells.length-1],key=ckey(...cell);
if(!crossingKeys(p).includes(key)||pathIndexesAtCell(st,key).length<2)return null;
const previous=path.cells[path.cells.length-2];return[cell[0]-previous[0],cell[1]-previous[1]];
}
function gateObj(p,i){const g=p.g[i];return{i,cell:[g[0],g[1]],side:g[2],internal:isInternalGateIndex(p,i)}}
function outsidePoint(g){if(g?.internal)return null;const[dR,dC]=SIDE_D[g.side];return[g.cell[0]+dR,g.cell[1]+dC]}
function detachedTurnAnalysis(path,p){
const cells=[],segments=[];let start=0;
for(let index=1;index<path.cells.length;index++)if(isWarpTransition(p,path.cells[index-1],path.cells[index])){segments.push(path.cells.slice(start,index));start=index}
segments.push(path.cells.slice(start));
for(const segment of segments)for(let index=1;index<segment.length-1;index++){const previous=segment[index-1],cell=segment[index],next=segment[index+1],before=[cell[0]-previous[0],cell[1]-previous[1]],after=[next[0]-cell[0],next[1]-cell[1]];if(before[0]!==after[0]||before[1]!==after[1])cells.push(cell)}
return{count:cells.length,cells};
}
function analyzeTurns(path,p,includeEnd){const special=specialCellSet(p),internal=(special.internalGates||[]).flatMap(pair=>[pair?.a,pair?.b]).filter(Number.isInteger);return path?.detachedStart?detachedTurnAnalysis(path,p):AppLogic.analyzePathTurns(path,p.g,special.warps,includeEnd,internal)}
function turnAnalysis(path,p){return analyzeTurns(path,p,true)}
function partialTurnCount(path,p){return analyzeTurns(path,p,false).count}
function numbersForPath(path,p){return p.n.filter(n=>path.cells.some(c=>c[0]===n[0]&&c[1]===n[1])).map(n=>({cell:[n[0],n[1]],value:n[2]}))}
function specialPathValid(path,p){
for(const pair of specialCellSet(p).warps){
const ai=path.cells.findIndex(cell=>sameCell(cell,pair.a)),bi=path.cells.findIndex(cell=>sameCell(cell,pair.b));
if((ai>=0)!==(bi>=0)||ai>=0&&Math.abs(ai-bi)!==1)return false;
}
for(const lock of specialCellSet(p).locks){
const keyIndex=path.cells.findIndex(cell=>sameCell(cell,lock.key)),doorIndex=path.cells.findIndex(cell=>sameCell(cell,lock.door));
if(doorIndex>=0&&(keyIndex<0||keyIndex>doorIndex))return false;
}
return true;
}
function pathValid(path,p){const nums=numbersForPath(path,p),ta=turnAnalysis(path,p);return specialPathValid(path,p)&&nums.length===1&&nums[0].value===ta.count&&ta.cells.some(c=>sameCell(c,nums[0].cell))}
function usedGateSet(st,except=-1){const s=new Set();st.paths.forEach((p,i)=>{if(i===except||!p)return;if(!p.detachedStart)s.add(p.startGate);if(p.endGate!=null)s.add(p.endGate);if(p.openGate!=null)s.add(p.openGate)});return s}
function occupiedMap(st){const m=new Map();st.paths.forEach((p,pi)=>p.cells.forEach(c=>m.set(ckey(...c),pi)));return m}
function isSolved(st,p){if(!crossingsSatisfied(st,p)||st.paths.length!==p.n.length||st.paths.some(x=>x.endGate==null||x.detachedStart))return false;if(usedGateSet(st).size!==p.g.length)return false;if(occupiedMap(st).size!==p.valid.length)return false;return st.paths.every(x=>pathValid(x,p))}
function sanitizeStateForPuzzle(meta,{quiet=false}={}){
const p=puzzleOf(meta),st=metaState(meta.id),validCells=cellSet(p),crossingSet=new Set(crossingKeys(p)),warpKeys=new Set([...warpMap(p).keys()]),usedCells=new Map(),usedGates=new Set(),kept=[];
let removed=0;
for(const rawPath of st.paths){
const path=normalizePath(rawPath);
let valid=!!path&&path.startGate>=0&&path.startGate<p.g.length&&path.endGate!==path.startGate&&(!path.detachedStart||path.endGate==null);
if(valid&&path.endGate!=null)valid=path.endGate>=0&&path.endGate<p.g.length;
if(valid){
const start=p.g[path.startGate];valid=path.detachedStart||sameCell(path.cells[0],start);
if(path.endGate!=null)valid=valid&&sameCell(path.cells[path.cells.length-1],p.g[path.endGate]);
}
const ownCells=new Set();
if(valid)for(let i=0;i<path.cells.length;i++){
const cell=path.cells[i],key=ckey(...cell),owners=usedCells.get(key)||0;
if(!validCells.has(key)||ownCells.has(key)||(owners>0&&!crossingSet.has(key))||owners>=2||(i>0&&!pathCellsAdjacent(p,path.cells[i-1],cell))){valid=false;break}
ownCells.add(key);
}
if(valid&&path.detachedStart&&path.endGate==null&&path.cells.length>1&&warpKeys.has(ckey(...path.cells[0]))&&warpKeys.has(ckey(...path.cells[path.cells.length-1])))valid=false;
if(valid&&(!path.detachedStart&&usedGates.has(path.startGate)||path.endGate!=null&&usedGates.has(path.endGate)||path.openGate!=null&&usedGates.has(path.openGate)))valid=false;
if(!valid){removed++;continue}
for(const key of ownCells)usedCells.set(key,(usedCells.get(key)||0)+1);
if(!path.detachedStart)usedGates.add(path.startGate);if(path.endGate!=null)usedGates.add(path.endGate);if(path.openGate!=null)usedGates.add(path.openGate);
Object.assign(rawPath,path);kept.push(rawPath);
}
if(removed){st.paths.splice(0,st.paths.length,...kept);st.rev=nextRevision();markStateDirty(meta.id);if(!quiet)loadNotices.push(`${meta.id}\u306e\u7834\u640d\u3057\u305f\u7dda\u3092${removed}\u672c\u524a\u9664\u3057\u307e\u3057\u305f\u3002`)}
if(st.specialProgress?.crossings?.length){st.specialProgress={crossings:[]};st.rev=nextRevision();markStateDirty(meta.id)}
if(st.solved&&st.store){const obstacleCell=storeObstacleCell(meta),current=Array.isArray(st.store.cell)?st.store.cell:null;if(obstacleCell&&(!current||!sameCell(current,obstacleCell))){st.store.cell=obstacleCell;st.store.pathIndex=-1;st.store.cellIndex=-1;delete st.store.summaryCell;st.rev=nextRevision();markStateDirty(meta.id)}}
const solved=st.solved===true||isSolved(st,p);
if(st.solved!==solved){st.solved=solved;st.rev=nextRevision();markStateDirty(meta.id)}
if(solved&&!st.solvedBy){st.solvedBy=LEGACY_LOCAL_SOLVER;st.rev=nextRevision();markStateDirty(meta.id)}
if(!solved&&(st.solvedBy||st.scoreAwarded||st.store)){st.solvedBy=null;st.scoreAwarded=0;st.store=null;st.rev=nextRevision();markStateDirty(meta.id)}
return removed;
}
function chooseColor(meta,gi){const inherited=neighborColor(meta,gi);return inherited??activeLineColorIndex()}
function pathColor(path,index=0){return LINE_COLORS[(path.colorIndex??index)%LINE_COLORS.length]}
function lineEffectClass(effect){return LINE_EFFECT_IDS.has(effect)?` line-effect-${effect}`:''}
function hexRgb(hex){const value=parseInt(String(hex).replace('#',''),16);return[(value>>16)&255,(value>>8)&255,value&255]}
function mixHex(a,b,t){const aa=hexRgb(a),bb=hexRgb(b),q=Math.max(0,Math.min(1,t)),value=aa.map((v,i)=>Math.round(v+(bb[i]-v)*q));return`#${value.map(v=>v.toString(16).padStart(2,'0')).join('')}`}
function levelBackground(level){const t=(Math.max(1,Math.min(10,level||1))-1)/9;return[ mixHex(DIFF_BACKGROUND_EASY[0],DIFF_BACKGROUND_HARD[0],t),mixHex(DIFF_BACKGROUND_EASY[1],DIFF_BACKGROUND_HARD[1],t) ]}
function pathDistanceProfile(path,p){
const samples=[];let total=0,previousPoint=path.detachedStart?boardCellCenter(path.cells[0]):gatePoint(gateObj(p,path.startGate)),previousCell=null;
for(const cell of path.cells||[]){
const point=boardCellCenter(cell);
if(!previousCell||!isWarpTransition(p,previousCell,cell))total+=Math.hypot(point[0]-previousPoint[0],point[1]-previousPoint[1]);
samples.push({cell,distance:total});previousPoint=point;previousCell=cell;
}
if(path.endGate!=null){const end=gatePoint(gateObj(p,path.endGate));total+=Math.hypot(end[0]-previousPoint[0],end[1]-previousPoint[1])}
return{samples,total};
}
function pathProgressAtCell(path,p,cell){
const profile=pathDistanceProfile(path,p),sample=profile.samples.find(entry=>sameCell(entry.cell,cell));
return sample&&profile.total>0?Math.max(0,Math.min(1,sample.distance/profile.total)):0;
}
function pathColorAtCell(path,p,cell){
const startIndex=path.startColorIndex??path.colorIndex??0,endIndex=path.endColorIndex??startIndex;
const colorEndGate=path.endGate??path.openGate;if(uiSettings.lightweightRendering||colorEndGate==null||startIndex===endIndex)return LINE_COLORS[startIndex%LINE_COLORS.length];
return mixHex(LINE_COLORS[startIndex%LINE_COLORS.length],LINE_COLORS[endIndex%LINE_COLORS.length],pathProgressAtCell(path,p,cell));
}
function straightNumberWarningKeys(st,p){
const numberKeys=new Set(p.n.map(number=>ckey(number[0],number[1]))),warnings=new Set();
for(const path of st.paths){
if(!path.cells.length)continue;
const startOutside=path.detachedStart?null:outsidePoint(gateObj(p,path.startGate)),points=[...(startOutside?[startOutside]:[]),...path.cells],offset=startOutside?1:0;
if(path.endGate!=null){const endOutside=outsidePoint(gateObj(p,path.endGate));if(endOutside)points.push(endOutside)}
path.cells.forEach((cell,index)=>{
const key=ckey(...cell),pointIndex=index+offset;
if(!numberKeys.has(key)||!points[pointIndex-1]||!points[pointIndex+1])return;
const before=[points[pointIndex][0]-points[pointIndex-1][0],points[pointIndex][1]-points[pointIndex-1][1]],
after=[points[pointIndex+1][0]-points[pointIndex][0],points[pointIndex+1][1]-points[pointIndex][1]];
if(before[0]===after[0]&&before[1]===after[1])warnings.add(key);
});
}
return warnings;
}
function multipleNumberWarningKeys(st,p){
const numberKeys=new Set(p.n.map(number=>ckey(number[0],number[1]))),warnings=new Set();
for(const path of st.paths){
const hits=path.cells.map(cell=>ckey(...cell)).filter(key=>numberKeys.has(key));
if(hits.length>1)for(const key of hits)warnings.add(key);
}
return warnings;
}
function formatScore(value){return Math.max(0,Math.round(Number(value)||0)).toLocaleString('en-US')}
function roundToThreeSignificantDigits(value){const amount=Math.max(0,Number(value)||0);if(amount<1000)return Math.round(amount);const unit=10**Math.max(0,Math.floor(Math.log10(amount))-2);return Math.round(amount/unit)*unit}
function formatProjectedScore(value){return formatScore(roundToThreeSignificantDigits(value))}
function lineGraphEnvironment(){return{data,metaState,matchingNeighborGate}}
function collectConnectedLineComponent(meta,pathIndex,cache=null){return AppLogic.collectConnectedLineComponent(meta,pathIndex,cache||currentLineGraphCaches().components,lineGraphEnvironment())}
function connectedLineLength(meta,pathIndex,cache=null){return AppLogic.connectedLineLength(meta,pathIndex,cache||currentLineGraphCaches().components,lineGraphEnvironment())}
function renderedConnectedLineWidth(meta,pathIndex,cache=null,componentCache=null){const caches=currentLineGraphCaches();return AppLogic.renderedConnectedLineWidth(meta,pathIndex,cache||caches.widths,lineGraphEnvironment(),componentCache||caches.components)}
function minimapGeometryForComponent(component,cache=currentLineGraphCaches().geometries){
if(cache.has(component))return cache.get(component);
const segments=[];
for(const[member,memberIndex]of component.members||[]){
if(!member?.puzzle)continue;const memberPath=metaState(member.id).paths[memberIndex];if(!memberPath?.cells?.length)continue;const memberPuzzle=puzzleOf(member),
gateMap=gateIndex=>{const g=gateObj(memberPuzzle,gateIndex),row=g.cell[0],col=g.cell[1];return[member.x+(col+(g.internal?.5:g.side==='E'?1:g.side==='W'?0:.5))/CHUNK,member.y+(row+(g.internal?.5:g.side==='S'?1:g.side==='N'?0:.5))/CHUNK]},turns=[];
for(let pointIndex=0;pointIndex<memberPath.cells.length;pointIndex++){const cell=memberPath.cells[pointIndex],previous=memberPath.cells[pointIndex-1],next=memberPath.cells[pointIndex+1];if(!previous||!next||(cell[1]-previous[1])*(next[0]-cell[0])!==(cell[0]-previous[0])*(next[1]-cell[1]))turns.push([member.x+(cell[1]+.5)/CHUNK,member.y+(cell[0]+.5)/CHUNK])}
const points=[gateMap(memberPath.startGate),...turns];if(memberPath.endGate!=null)points.push(gateMap(memberPath.endGate));segments.push({memberId:member.id,pathIndex:memberIndex,points,color:pathColor(memberPath,memberIndex)});
}
const geometry={length:component.length,segments};cache.set(component,geometry);perfCount('minimapGeometryBuilds');return geometry;
}
function scheduleMinimap(markDirty=false){
if(markDirty)minimapDirty=true;
if(!minimapCanvas||!minimapDirty||minimapFrame)return;
// Panning never needs a live minimap repaint. The viewport indicator is
// refreshed once when the gesture ends, avoiding a second map render loop.
if(pan||pinch||interactionActive('overview'))return;
const step=timestamp=>{
if(!minimapFrame)return;
if(interactionActive('overview')){minimapFrame=0;return}
if(timestamp-minimapLastDraw<AUXILIARY_FRAME_INTERVAL-1){minimapFrame=requestAnimationFrame(step);return}
minimapFrame=0;minimapLastDraw=timestamp;markVisualFrame(timestamp);drawMinimap();
};
minimapFrame=requestAnimationFrame(step);
}
function cameraCenterInChunks(){
const r=getViewportRect();
return[renderOriginX+(r.width/2-cam.x)/(UNIT*cam.scale),renderOriginY+(r.height/2-cam.y)/(UNIT*cam.scale)];
}
function drawMapBoardCells(context,metas,mapX,mapY,unitSize,{showLevels=false}={}){
const overlap=Math.max(.35,Math.min(1,unitSize*.08)),unsolvedRects=[],solvedRects=[],levelLabels=[];
for(const meta of metas){
const solved=metaState(meta.id).solved,rects=solved?solvedRects:unsolvedRects;let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
for(const[dx,dy]of meta.chunks){const worldX=meta.x+dx,worldY=meta.y+dy,x=mapX(worldX)-overlap/2,y=mapY(worldY)-overlap/2;rects.push([x,y,Math.max(1,unitSize+overlap),Math.max(1,unitSize+overlap)]);minX=Math.min(minX,worldX);minY=Math.min(minY,worldY);maxX=Math.max(maxX,worldX+1);maxY=Math.max(maxY,worldY+1)}
if(showLevels&&!solved&&minX<Infinity)levelLabels.push({text:String(Math.max(1,Math.min(10,Number(meta.level)||1))),x:mapX((minX+maxX)/2),y:mapY((minY+maxY)/2)});
}
const fillRects=(rects,color)=>{if(!rects.length)return;context.beginPath();for(const[x,y,w,h]of rects)context.rect(x,y,w,h);context.fillStyle=color;context.fill()};
fillRects(unsolvedRects,'#30373c');fillRects(solvedRects,'#477a5d');
if(showLevels&&levelLabels.length){const fontSize=Math.max(8,Math.min(22,unitSize*.42));context.save();context.font=`900 ${fontSize}px DotGothic16Local,monospace`;context.textAlign='center';context.textBaseline='middle';context.lineJoin='round';context.lineWidth=Math.max(2,fontSize*.18);context.strokeStyle='rgba(7,10,12,.88)';context.fillStyle='rgba(245,249,252,.96)';for(const label of levelLabels){context.strokeText(label.text,label.x,label.y);context.fillText(label.text,label.x,label.y)}context.restore()}
}
function drawMapStores(context,metas,mapX,mapY,unitSize){
let count=0;context.save();context.lineJoin='round';
for(const meta of metas){const store=metaState(meta.id).store;if(!store)continue;const[xWorld,yWorld]=storePriceLocation(meta,store),x=mapX(xWorld),y=mapY(yWorld),radius=Math.max(3,Math.min(10,unitSize*.18));count++;
context.beginPath();context.moveTo(x,y-radius*1.45);context.lineTo(x+radius*1.2,y);context.lineTo(x,y+radius*1.45);context.lineTo(x-radius*1.2,y);context.closePath();context.fillStyle='#ff46c7';context.fill();context.lineWidth=Math.max(1,radius*.28);context.strokeStyle='#6ffcff';context.stroke();
context.beginPath();context.arc(x,y,radius*.34,0,Math.PI*2);context.fillStyle='#11151c';context.fill();
}
context.restore();return count;
}
function nearestStoreMetaAtWorldPoint(worldX,worldY,pixelRadius,worldPixelsPerChunk){
const radius=Math.max(.05,pixelRadius/Math.max(1,worldPixelsPerChunk));let best=null,bestDistance=radius*radius;
for(const meta of Object.values(data.metas||{})){const store=metaState(meta.id).store;if(!store)continue;const[x,y]=storePriceLocation(meta,store),distance=(x-worldX)**2+(y-worldY)**2;if(distance<=bestDistance){bestDistance=distance;best=meta}}
return best;
}
function drawMapLongLines(context,metas,visibleIds,mapX,mapY,lineWidth=1){
if(metas.length>96)return 0;let longSegments=0;const caches=currentLineGraphCaches(),seenComponents=new Set();context.lineCap='square';context.lineJoin='miter';
for(const meta of metas){if(!meta.puzzle)continue;const st=metaState(meta.id);for(let index=0;index<st.paths.length;index++){
const component=collectConnectedLineComponent(meta,index,caches.components);if(component.length<=MINIMAP_LONG_LINE||seenComponents.has(component))continue;seenComponents.add(component);longSegments++;
const geometry=minimapGeometryForComponent(component,caches.geometries);for(const segment of geometry.segments){if(!visibleIds.has(segment.memberId))continue;context.beginPath();segment.points.forEach((point,i)=>(i?context.lineTo(mapX(point[0]),mapY(point[1])):context.moveTo(mapX(point[0]),mapY(point[1]))));context.strokeStyle=segment.color;context.lineWidth=lineWidth;context.globalAlpha=.9;context.stroke();context.globalAlpha=1}
}}return longSegments
}
function rebuildMinimapWorld(width,height,dpr,centerX,centerY){
const started=perfStart(),spanX=MINIMAP_VIEW_CHUNKS_X,spanY=Math.max(10,spanX*height/Math.max(1,width)),scale=Math.min(width/spanX,height/spanY),overscan=MINIMAP_CACHE_OVERSCAN_CHUNKS,overscanPixels=overscan*scale,
baseWidth=width+overscanPixels*2,baseHeight=height+overscanPixels*2,minChunkX=centerX-spanX/2-overscan,minChunkY=centerY-spanY/2-overscan,maxChunkX=centerX+spanX/2+overscan,maxChunkY=centerY+spanY/2+overscan,
mapX=value=>baseWidth/2+(value-centerX)*scale,mapY=value=>baseHeight/2+(value-centerY)*scale,pixelWidth=Math.round(baseWidth*dpr),pixelHeight=Math.round(baseHeight*dpr);
if(minimapBase.width!==pixelWidth||minimapBase.height!==pixelHeight){minimapBase.width=pixelWidth;minimapBase.height=pixelHeight}
const base=minimapBase.getContext('2d');base.setTransform(dpr,0,0,dpr,0,0);base.clearRect(0,0,baseWidth,baseHeight);base.fillStyle='#101315';base.fillRect(0,0,baseWidth,baseHeight);
const visibleIds=new Set();for(let y=Math.floor(minChunkY)-1;y<=Math.ceil(maxChunkY)+1;y++)for(let x=Math.floor(minChunkX)-1;x<=Math.ceil(maxChunkX)+1;x++){const id=occupancy.get(key2(x,y));if(id)visibleIds.add(id)}
const visibleMetas=[...visibleIds].map(id=>data.metas[id]).filter(Boolean);drawMapBoardCells(base,visibleMetas,mapX,mapY,scale);const longSegments=drawMapLongLines(base,visibleMetas,visibleIds,mapX,mapY,1),storeCount=drawMapStores(base,visibleMetas,mapX,mapY,scale);
minimapCache={revision:minimapWorldRevision,width,height,dpr,anchorX:centerX,anchorY:centerY,scale,overscanPixels,baseWidth,baseHeight,longSegments,storeCount};perfCount('minimapWorldBuilds');perfEnd('rebuildMinimapWorld',started);
}
function drawMinimap(){
if(interactionActive('overview')){minimapDirty=true;perfCount('minimapDrawsDeferredDuringInteraction');return false}
const started=perfStart();perfCount('minimapDraws');const rect=getMinimapRect(),width=Math.max(1,Math.round(rect.width)),height=Math.max(1,Math.round(rect.height)),dpr=Math.min(2,Math.max(1,window.devicePixelRatio||1)),pixelWidth=Math.round(width*dpr),pixelHeight=Math.round(height*dpr),[centerX,centerY]=cameraCenterInChunks();
if(minimapCanvas.width!==pixelWidth||minimapCanvas.height!==pixelHeight){minimapCanvas.width=pixelWidth;minimapCanvas.height=pixelHeight}
const stale=!minimapCache||minimapCache.revision!==minimapWorldRevision||minimapCache.width!==width||minimapCache.height!==height||minimapCache.dpr!==dpr||
Math.abs(centerX-minimapCache.anchorX)*minimapCache.scale>minimapCache.overscanPixels*.75||Math.abs(centerY-minimapCache.anchorY)*minimapCache.scale>minimapCache.overscanPixels*.75;
if(stale)rebuildMinimapWorld(width,height,dpr,centerX,centerY);
minimapDirty=false;minimapLongSegments=minimapCache.longSegments;
const sourceX=(minimapCache.overscanPixels+(centerX-minimapCache.anchorX)*minimapCache.scale)*dpr,sourceY=(minimapCache.overscanPixels+(centerY-minimapCache.anchorY)*minimapCache.scale)*dpr,
context=minimapCanvas.getContext('2d');context.setTransform(1,0,0,1,0,0);context.clearRect(0,0,pixelWidth,pixelHeight);context.drawImage(minimapBase,sourceX,sourceY,pixelWidth,pixelHeight,0,0,pixelWidth,pixelHeight);
context.setTransform(dpr,0,0,dpr,0,0);context.strokeStyle='rgba(239,248,255,.82)';context.lineWidth=1;context.beginPath();context.moveTo(width/2-5,height/2);context.lineTo(width/2+5,height/2);context.moveTo(width/2,height/2-5);context.lineTo(width/2,height/2+5);context.stroke();
for(const player of(typeof remotePlayers==='undefined'?[]:remotePlayers.values())){const worldX=Number.isFinite(player.currentX)?player.currentX:player.targetX,worldY=Number.isFinite(player.currentY)?player.currentY:player.targetY;if(!Number.isFinite(worldX)||!Number.isFinite(worldY))continue;const x=width/2+(worldX-centerX)*minimapCache.scale,y=height/2+(worldY-centerY)*minimapCache.scale;if(x<-4||y<-4||x>width+4||y>height+4)continue;context.beginPath();context.arc(x,y,2.6,0,Math.PI*2);context.fillStyle='#d9f06f';context.fill();context.strokeStyle='rgba(8,12,15,.9)';context.lineWidth=1;context.stroke()}
const label='マップ。店舗マーカーをクリックするとショップを開きます。';
if(minimapCanvas.getAttribute('aria-label')!==label)minimapCanvas.setAttribute('aria-label',label);
perfCount(stale?'minimapCacheMisses':'minimapCacheHits');perfEnd('drawMinimap',started);
}
function worldNavigationBounds(){
const bounds=fieldBoundsFromMetas(data.metas),margin=3;return{minX:bounds.minX-margin,maxX:bounds.maxX+margin,minY:bounds.minY-margin,maxY:bounds.maxY+margin};
}
function centerWorldUnit(worldX,worldY,{persist=true}={}){
if(!Number.isFinite(worldX)||!Number.isFinite(worldY))return false;const bounds=worldNavigationBounds(),rect=getViewportRect();
worldX=Math.max(bounds.minX,Math.min(bounds.maxX,worldX));worldY=Math.max(bounds.minY,Math.min(bounds.maxY,worldY));
worldOverviewActive=false;cam.x=rect.width/2-(worldX-renderOriginX)*UNIT*cam.scale;cam.y=rect.height/2-(worldY-renderOriginY)*UNIT*cam.scale;applyCamera(true);ensureBoards();if(persist)recordCameraAnchor();return true;
}
function minimapPoint(event){
const rect=getMinimapRect();return{x:event.clientX-rect.left,y:event.clientY-rect.top,width:rect.width,height:rect.height};
}
function minimapWorldPoint(event){
const point=minimapPoint(event),center=cameraCenterInChunks(),scale=minimapCache?.scale||Math.max(1,point.width/MINIMAP_VIEW_CHUNKS_X);
return{x:center[0]+(point.x-point.width/2)/scale,y:center[1]+(point.y-point.height/2)/scale,scale};
}
function beginMinimapPointer(event){
if(event.button!==0||!gestureCoordinator.claim(event.pointerId,'minimap'))return;event.preventDefault();event.stopPropagation();const target=minimapWorldPoint(event),storeMeta=nearestStoreMetaAtWorldPoint(target.x,target.y,14,target.scale);
if(storeMeta){gestureCoordinator.release(event.pointerId,'minimap');playSound('shop');centerMeta(storeMeta,{preserveScale:true,select:false});openStoreMeta(storeMeta);minimapCanvas.focus({preventScroll:true});return}
minimapPointerState={id:event.pointerId,startClientX:event.clientX,startClientY:event.clientY,targetX:target.x,targetY:target.y,scale:target.scale};
interactionState.set('minimap',event.pointerId);
try{minimapCanvas.setPointerCapture(event.pointerId)}catch(_){}
centerWorldUnit(target.x,target.y,{persist:false});minimapCanvas.focus({preventScroll:true});
}
function moveMinimapPointer(event){
const state=minimapPointerState;if(!state||state.id!==event.pointerId)return;event.preventDefault();
centerWorldUnit(state.targetX+(event.clientX-state.startClientX)/state.scale,state.targetY+(event.clientY-state.startClientY)/state.scale,{persist:false});
}
function endMinimapPointer(event){
if(!minimapPointerState||minimapPointerState.id!==event.pointerId)return;minimapPointerState=null;
try{if(minimapCanvas.hasPointerCapture?.(event.pointerId))minimapCanvas.releasePointerCapture(event.pointerId)}catch(_){}
gestureCoordinator.release(event.pointerId,'minimap');interactionState.clear('minimap');recordCameraAnchor();
}
function keyMinimap(event){
const directions={ArrowUp:[0,-1],ArrowDown:[0,1],ArrowLeft:[-1,0],ArrowRight:[1,0]},direction=directions[event.key];
if(!direction)return;event.preventDefault();const center=cameraCenterInChunks(),step=event.shiftKey?5:1;centerWorldUnit(center[0]+direction[0]*step,center[1]+direction[1]*step);
}
function scoreFromThickness(meta,totalThickness){
const level=Math.max(1,Math.min(10,meta.level||1)),
sections=Math.max(1,meta.chunks?.length||Math.ceil((((meta.puzzle?.valid?.length||0)+(meta.puzzle?.obstacles?.length||0))||25)/25)),
hardMultiplier=1+Math.max(0,level-4)*.18,
largeMultiplier=1+Math.pow(Math.max(0,sections-1),.82)*(.18+level*.018),
effortMultiplier=hardMultiplier*largeMultiplier,
raw=level*level*100+totalThickness*level*20,
originalAward=Math.max(100,Math.round(raw/10)*10),
quarter=Math.max(25,Math.round(originalAward/4)),
halved=Math.max(13,Math.round(quarter/2));
return Math.min(MAX_SCORE,Math.max(13,Math.round(halved*effortMultiplier)));
}
function scoreForBoard(meta){
const st=metaState(meta.id);
if(!st.solved||!st.paths.length)return 0;
const cache=new Map(),totalThickness=st.paths.reduce((sum,_,index)=>sum+lineStrokeWidth(connectedLineLength(meta,index,cache)),0);
return scoreFromThickness(meta,totalThickness);
}
function scoreLensCountForMeta(){return activeScoreLensCount()}
function timeAttackRewardModifier(){
const run=data.timeAttack;if(!run||trustedNow()>=run.endsAt)return 1;
return timeAttackMultiplier(run.baseCollected||0);
}
function rewardDetailsForBoard(meta,state=metaState(meta.id)){
const baseScore=scoreForBoard(meta),options={worldSeed:worldSeedValue(),meta,state,scoreLensCount:scoreLensCountForMeta(meta)},
beforeTime=AppLogic.deterministicBoardReward(baseScore,{...options,timeAttackModifier:1}),
reward=AppLogic.deterministicBoardReward(baseScore,{...options,timeAttackModifier:timeAttackRewardModifier()});
return{...reward,preTimeAward:beforeTime.award};
}
function projectedScoreForBoard(meta){
const solution=puzzleOf(meta).solution||[],baseScore=scoreFromThickness(meta,solution.reduce((sum,path)=>sum+lineStrokeWidth(path.cells.length),0)),
projectedState={paths:solution,solved:true};
return AppLogic.deterministicBoardReward(baseScore,{worldSeed:worldSeedValue(),meta,state:projectedState,timeAttackModifier:timeAttackRewardModifier(),scoreLensCount:scoreLensCountForMeta(meta)}).award;
}
function storeObstacleCell(meta){const obstacles=puzzleOf(meta).obstacles||[];if(!obstacles.length)return null;return[...obstacles[(hash32((meta.seed>>>0)^0x2fd51a37)>>>0)%obstacles.length]]}
function maybeOpenStore(meta,st,solveAward,roll=null){
const deterministicRoll=roll==null?(hash32(meta.seed^0x7f4a7c15)>>>0)/4294967296:roll,cell=storeObstacleCell(meta);
if(!st.solved||st.store||!cell||deterministicRoll>=STORE_CHANCE)return 0;
const bonus=0;
st.store={owner:currentPlayerName(),pathIndex:-1,cellIndex:-1,cell,openedAt:trustedNow(),priceVersion:STORE_PRICE_VERSION,priceCoefficient:null,bonus,bonusVersion:SCORE_VERSION,itemIds:seededStoreItemIds(meta.seed),purchases:[]};
st.store.priceCoefficient=storePriceDetails(meta,st.store).coefficient;return bonus;
}
function gatePoint(g){const r=g.cell[0],c=g.cell[1],x=PAD+(c+.5)*CELL,y=PAD+(r+.5)*CELL;if(g.internal)return[x,y];return g.side==='N'?[x,PAD+r*CELL]:g.side==='S'?[x,PAD+(r+1)*CELL]:g.side==='W'?[PAD+c*CELL,y]:[PAD+(c+1)*CELL,y]}
function gateMarkerPathAt(point,side,sealed=false,scale=1,internal=false){
if(internal){const[dr,dc]=SIDE_D[side],tx=-dr,ty=dc,stem=10*scale,wing=5*scale,tip=[point[0]+dc*stem,point[1]+dr*stem],left=[point[0]+tx*wing,point[1]+ty*wing],right=[point[0]-tx*wing,point[1]-ty*wing];return`M ${left[0]} ${left[1]} L ${point[0]} ${point[1]} L ${right[0]} ${right[1]} M ${point[0]} ${point[1]} L ${tip[0]} ${tip[1]}`}
const[dr,dc]=SIDE_D[side],ox=dc,oy=dr,tx=-dr,ty=dc,stem=11*scale,back=4.5*scale,wing=4.5*scale,
tip=[point[0]+ox*stem,point[1]+oy*stem];
if(sealed){
const center=[point[0]+ox*7*scale,point[1]+oy*7*scale],
left=[center[0]+tx*wing,center[1]+ty*wing],right=[center[0]-tx*wing,center[1]-ty*wing];
return`M ${point[0]} ${point[1]} L ${center[0]} ${center[1]} M ${left[0]} ${left[1]} L ${right[0]} ${right[1]}`;
}
const base=[tip[0]-ox*back,tip[1]-oy*back],
left=[base[0]+tx*wing,base[1]+ty*wing],right=[base[0]-tx*wing,base[1]-ty*wing];
return`M ${point[0]} ${point[1]} L ${tip[0]} ${tip[1]} M ${left[0]} ${left[1]} L ${tip[0]} ${tip[1]} L ${right[0]} ${right[1]}`;
}
function gateHitBox(point,side,internal=false){
if(internal){const half=Math.round(CELL*.38);return{x:point[0]-half,y:point[1]-half,width:half*2,height:half*2}}
// Keep hit regions inside the owning board. Outward hit boxes from adjacent
// boards overlapped and could steal a pointer from the opposite gate.
const inward=Math.round(CELL*.42),outward=0,half=Math.round(CELL*.30),depth=inward+outward;
if(side==='N')return{x:point[0]-half,y:point[1],width:half*2,height:depth};
if(side==='S')return{x:point[0]-half,y:point[1]-inward,width:half*2,height:depth};
if(side==='W')return{x:point[0],y:point[1]-half,width:depth,height:half*2};
return{x:point[0]-inward,y:point[1]-half,width:depth,height:half*2};
}
function gateAtCell(p,cell,exclude=null,side=null){if(!p._gateCells){p._gateCells=new Map();p.g.forEach((g,i)=>{const key=ckey(g[0],g[1]);if(!p._gateCells.has(key))p._gateCells.set(key,[]);p._gateCells.get(key).push(i)})}for(const i of p._gateCells.get(ckey(...cell))||[])if(i!==exclude&&(!side||p.g[i][2]===side))return i;return null}
function regionFor(level){return REGIONS[Math.min(REGIONS.length-1,Math.floor((Math.max(1,level)-1)/2))]}
function hudPlacementCandidates(meta){
const own=new Set(meta.chunks.map(([dx,dy])=>key2(meta.x+dx,meta.y+dy))),priority={N:0,S:1,W:2,E:3},candidates=[];
for(const[dx,dy]of meta.chunks)for(const side of['N','S','W','E']){
const[dr,dc]=SIDE_D[side],localKey=key2(dx+dc,dy+dr);if(own.has(key2(meta.x+dx+dc,meta.y+dy+dr)))continue;
candidates.push({side,dx,dy,priority:priority[side],distance:Math.abs(dx+.5-(puzzleOf(meta).bounds.w/CHUNK)/2)+Math.abs(dy+.5-(puzzleOf(meta).bounds.h/CHUNK)/2),localKey});
}
return candidates.sort((a,b)=>a.priority-b.priority||a.distance-b.distance||a.dy-b.dy||a.dx-b.dx);
}
function positionBoardLabel(b){
if(!b?.label||!boardHudLayer||!boardPlayHudVisible(b))return false;
const candidates=hudPlacementCandidates(b.meta),fallback={side:'N',dx:0,dy:0,priority:0},viewportRect=getViewportRect(true),boardRect=boardScreenRect(b),scale=Math.max(MIN_CAMERA_SCALE,cam.scale),margin=8,gap=10,
innerWidth=b.p.bounds.w*CELL,label=b.label;
label.hidden=false;label.classList.add('hud-visible');label.style.removeProperty('width');label.style.maxWidth=Math.max(120,viewportRect.width-margin*2)+'px';
const labelWidth=Math.min(viewportRect.width-margin*2,Math.max(1,label.offsetWidth||190)),labelHeight=Math.min(viewportRect.height-margin*2,Math.max(1,label.offsetHeight||34));
const anchorFor=placement=>{
const side=placement.side||'N',horizontal=side==='N'||side==='S',
x=horizontal?boardRect.left-viewportRect.left+(PAD+innerWidth/2)*scale:boardRect.left-viewportRect.left+(PAD+(placement.dx+(side==='E'?1:0))*UNIT)*scale,
y=horizontal?boardRect.top-viewportRect.top+(PAD+(side==='S'?b.p.bounds.h*CELL:0))*scale:boardRect.top-viewportRect.top+(PAD+(placement.dy+.5)*UNIT)*scale;
if(side==='N')return{left:x-labelWidth/2,top:y-gap-labelHeight};
if(side==='S')return{left:x-labelWidth/2,top:y+gap};
if(side==='W')return{left:x-gap-labelWidth,top:y-labelHeight/2};
return{left:x+gap,top:y-labelHeight/2};
};
const overflow=point=>Math.max(0,margin-point.left)+Math.max(0,point.left+labelWidth-(viewportRect.width-margin))+Math.max(0,margin-point.top)+Math.max(0,point.top+labelHeight-(viewportRect.height-margin));
const ranked=(candidates.length?candidates:[fallback]).map(candidate=>({candidate,point:anchorFor(candidate)}));ranked.sort((a,c)=>overflow(a.point)-overflow(c.point)||(a.candidate.priority||0)-(c.candidate.priority||0));
const selected=ranked[0],left=Math.max(margin,Math.min(selected.point.left,viewportRect.width-margin-labelWidth)),top=Math.max(margin,Math.min(selected.point.top,viewportRect.height-margin-labelHeight));
label.dataset.side=selected.candidate.side;label.style.left=left.toFixed(2)+'px';label.style.top=top.toFixed(2)+'px';label.style.transform='none';return true;
}
const dirtyBoards=new Set(),lineWidthDirtyPathKeys=new Set();let renderFrame=0,lineWidthFrame=0;
function applyLineWidth(node,width){const value=width.toFixed(2);if(node.style.getPropertyValue('--line-width')!==value)node.style.setProperty('--line-width',value)}
function linePathKey(id,index){return`${id}:${index}`}
function splitLinePathKey(key){const split=key.lastIndexOf(':');return split<0?[key,NaN]:[key.slice(0,split),Number(key.slice(split+1))]}
function refreshRenderedLineWidths(pathKeys=null){
const started=perfStart(),caches=currentLineGraphCaches(),refreshAll=pathKeys==null,targetKeys=pathKeys?new Set(pathKeys):new Set();
if(refreshAll)for(const[id,board]of rendered)for(let index=0;index<metaState(id).paths.length;index++)targetKeys.add(linePathKey(board.id,index));
for(const key of[...targetKeys]){
const[id,index]=splitLinePathKey(key),meta=data.metas[id];if(!meta?.puzzle||!Number.isInteger(index)||!metaState(id).paths[index])continue;
for(const[member,memberIndex]of collectConnectedLineComponent(meta,index,caches.components).members)targetKeys.add(linePathKey(member.id,memberIndex));
}
const targetIds=new Set([...targetKeys].map(key=>splitLinePathKey(key)[0]));
for(const id of targetIds){
const board=rendered.get(id);if(!board)continue;
const pathNodes=board.pathStrokeNodes||[];
for(const node of pathNodes){
const index=Number(node.dataset.pathIndex);if(!targetKeys.has(linePathKey(id,index)))continue;
const width=renderedConnectedLineWidth(board.meta,index,caches.widths,caches.components);applyLineWidth(node,width);
}
const connectorNodes=board.connectorNodes||[];
for(const node of connectorNodes){
const ownerId=node.dataset.ownerBoard||board.id,index=Number(node.dataset.pathIndex),owner=data.metas[ownerId];
if(!owner||!Number.isInteger(index)||!targetKeys.has(linePathKey(ownerId,index)))continue;applyLineWidth(node,renderedConnectedLineWidth(owner,index,caches.widths,caches.components));
}
}
perfGauge('lineWidthBoards',targetIds.size);perfGauge('lineWidthPaths',targetKeys.size);perfEnd('refreshRenderedLineWidths',started);
}
function queueLineWidthRefresh(boardId=null,pathIndex=null){
if(boardId){if(Number.isInteger(pathIndex))lineWidthDirtyPathKeys.add(linePathKey(boardId,pathIndex));else for(let index=0;index<metaState(boardId).paths.length;index++)lineWidthDirtyPathKeys.add(linePathKey(boardId,index))}
if(lineWidthFrame)return;
lineWidthFrame=requestAnimationFrame(()=>{lineWidthFrame=0;const keys=new Set(lineWidthDirtyPathKeys);lineWidthDirtyPathKeys.clear();refreshRenderedLineWidths(keys)});
}
function commitConnectedLineVisuals(b,pathIndex=null){
if(!b)return false;invalidateLineGraphCaches(b.id,pathIndex);if(b.card?.isConnected)renderBoard(b);if(Number.isInteger(pathIndex))queueLineWidthRefresh(b.id,pathIndex);else queueLineWidthRefresh(b.id);return true
}
function scoreLensVisibleForMeta(meta){return cam.scale<=SCORE_LENS_ZOOM_THRESHOLD&&!metaState(meta.id).solved&&activeScoreLensCount()>0}
function updateScoreLensBadge(b){
if(!b?.scoreLensBadge)return;const visible=scoreLensVisibleForMeta(b.meta);b.scoreLensBadge.hidden=!visible;
if(visible){const score=formatProjectedScore(projectedScoreForBoard(b.meta));b.scoreLensBadge.textContent=`${score}`;b.scoreLensBadge.setAttribute('aria-label',`予想報酬 ${score}`);b.scoreLensBadge.setAttribute('aria-hidden','false')}else{b.scoreLensBadge.removeAttribute('aria-label');b.scoreLensBadge.setAttribute('aria-hidden','true')}
}
let lastPresentedScale=NaN,worldOverviewActive=false,zoomDetailsDirty=false;
function inWorldOverview(){
const drawingActive=document.body.classList.contains('is-drawing')||Boolean(rendered.get(activeBoard)?.drawing?.keyboardActive);
if(drawingActive){worldOverviewActive=false;return false}
worldOverviewActive=cam.scale<=OVERVIEW_ZOOM_THRESHOLD;
return worldOverviewActive;
}
function updateZoomPresentation(force=false){
if(!force&&Math.abs(cam.scale-lastPresentedScale)<1e-6)return inWorldOverview();lastPresentedScale=cam.scale;
const overview=inWorldOverview();world.classList.toggle('world-overview',overview);world.classList.toggle('score-lens-zoom',cam.scale<=SCORE_LENS_ZOOM_THRESHOLD);viewport.classList.toggle('canvas-overview',overview);
world.style.setProperty('--inverse-camera-scale',String(1/Math.max(MIN_CAMERA_SCALE,cam.scale)));world.style.setProperty('--camera-scale',String(cam.scale));
if(interactionActive('overview'))zoomDetailsDirty=true;else{for(const board of rendered.values())updateScoreLensBadge(board);zoomDetailsDirty=false}
if(overview){overviewDirty=true;scheduleWorldOverview(true)}else if(overviewCanvas){overviewCanvas.hidden=true}
return overview;
}
function flushBoardRenderQueue(timestamp){
renderFrame=0;if(!dirtyBoards.size)return;markVisualFrame(timestamp);let count=0;
for(const board of [...dirtyBoards]){dirtyBoards.delete(board);if(board.card.isConnected)renderBoardNow(board);if(++count>=BOARD_RENDERS_PER_FRAME)break}
if(dirtyBoards.size)renderFrame=requestAnimationFrame(flushBoardRenderQueue);
}
function renderBoard(b){
if(!b||!b.card.isConnected)return;dirtyBoards.add(b);perfCount('boardRenderRequests');if(!renderFrame)renderFrame=requestAnimationFrame(flushBoardRenderQueue);
}
function boardCellCenter(cell){return[PAD+(cell[1]+.5)*CELL,PAD+(cell[0]+.5)*CELL]}
function pathRenderSegments(path,p){
if(!path?.cells?.length)return[];const segments=[],first=boardCellCenter(path.cells[0]);let current=path.detachedStart?[first]:[gatePoint(gateObj(p,path.startGate)),first];
for(let index=1;index<path.cells.length;index++){
const previous=path.cells[index-1],cell=path.cells[index];
if(isWarpTransition(p,previous,cell)){segments.push(current);current=[boardCellCenter(cell)]}
else current.push(boardCellCenter(cell));
}
if(path.endGate!=null)current.push(gatePoint(gateObj(p,path.endGate)));segments.push(current);return segments.filter(segment=>segment.length>0);
}
function pathStrokePieces(segments,startColor,endColor){
const pieces=[];let total=0;
for(const points of segments)for(let i=1;i<points.length;i++)total+=Math.hypot(points[i][0]-points[i-1][0],points[i][1]-points[i-1][1]);
let distance=0;
for(const points of segments){
if(points.length<2)continue;
let segmentLength=0;for(let i=1;i<points.length;i++)segmentLength+=Math.hypot(points[i][0]-points[i-1][0],points[i][1]-points[i-1][1]);
if(segmentLength<=0)continue;
const startProgress=total?distance/total:0;distance+=segmentLength;const endProgress=total?distance/total:1;
pieces.push({points,startColor:mixHex(startColor,endColor,startProgress),endColor:mixHex(startColor,endColor,endProgress),startProgress,endProgress});
}
return pieces;
}
function specialCellInfoMap(p){
const map=new Map(),special=specialCellSet(p);
for(const cell of special.crossings)map.set(ckey(...cell),{type:'cross',description:'\u4ea4\u5dee\u30bb\u30eb\uff1a\u30af\u30ea\u30a2\u6642\u306b\u6c34\u5e73\u3068\u5782\u76f4\u306e2\u672c\u3092\u4ea4\u5dee\u72b6\u614b\u306b\u3059\u308b'});
for(const pair of special.warps)for(const cell of[pair.a,pair.b])map.set(ckey(...cell),{type:'warp',description:'\u30ef\u30fc\u30d7\u30bb\u30eb\uff1a\u5165\u308b\u3068\u5bfe\u306b\u306a\u308b\u30bb\u30eb\u3078\u79fb\u52d5'});
for(const lock of special.locks){map.set(ckey(...lock.key),{type:'key',description:'\u9375\u30bb\u30eb\uff1a\u540c\u3058\u7dda\u3067\u89e6\u308c\u308b\u3068\u6249\u3092\u901a\u904e\u53ef\u80fd'});map.set(ckey(...lock.door),{type:'door',description:'\u6249\u30bb\u30eb\uff1a\u9375\u306b\u89e6\u308c\u305f\u7dda\u306e\u307f\u901a\u904e\u53ef\u80fd'})}
for(const pair of special.internalGates||[])for(const index of[pair.a,pair.b]){const gate=p.g?.[index];if(gate)map.set(ckey(gate[0],gate[1]),{type:'internal-gate',description:'盤面内ゲート:つまみから通常ゲートと同様に線を引く'})}
return map;
}
function makeSpecialMarker(info,r,c){
const x=PAD+c*CELL,y=PAD+r*CELL,cx=x+CELL/2,cy=y+CELL/2,group=svgEl('g',{class:`special-cell special-${info.type}`,'data-special-cell':ckey(r,c),'aria-hidden':'true'});
group.append(svgEl('rect',{x:x+4,y:y+4,width:CELL-8,height:CELL-8,rx:3,class:'special-cell-frame'}));
if(info.type==='cross')group.append(svgEl('line',{x1:cx-11,y1:cy,x2:cx+11,y2:cy,class:'special-symbol-line'}),svgEl('line',{x1:cx,y1:cy-11,x2:cx,y2:cy+11,class:'special-symbol-line'}),svgEl('circle',{cx,cy,r:4,class:'special-symbol-core'}));
else if(info.type==='warp')group.append(svgEl('circle',{cx,cy,r:11,class:'warp-ring'}),svgEl('circle',{cx,cy,r:5,class:'warp-core'}));
else if(info.type==='internal-gate')group.append(svgEl('circle',{cx,cy,r:13,class:'internal-gate-bracket'}),svgEl('circle',{cx,cy,r:8,class:'internal-gate-bracket inner'}));
else if(info.type==='key')group.append(svgEl('circle',{cx:cx-5,cy:cy-3,r:5.2,class:'key-ring'}),svgEl('line',{x1:cx-1,y1:cy+1,x2:cx+9,y2:cy+11,class:'key-shaft'}),svgEl('line',{x1:cx+5,y1:cy+7,x2:cx+9,y2:cy+3,class:'key-tooth'}),svgEl('line',{x1:cx+8,y1:cy+10,x2:cx+12,y2:cy+6,class:'key-tooth'}))
else{group.append(svgEl('rect',{x:cx-9,y:cy-12,width:18,height:24,rx:2,class:'door-panel'}),svgEl('circle',{cx:cx+4,cy,r:1.8,class:'door-knob'}))}
return group;
}
function buildTutorialBoards(){
const tutorialCell=34,tutorialPad=16;
function addGrid(svg,{x=tutorialPad,y=tutorialPad,cols=4,rows=3,bg=levelBackground(1)[0]}={}){
for(let row=0;row<rows;row++)for(let col=0;col<cols;col++)
svg.append(svgEl('rect',{x:x+col*tutorialCell,y:y+row*tutorialCell,width:tutorialCell,height:tutorialCell,class:'cell-bg tutorial-cell',fill:bg}));
svg.append(svgEl('rect',{x,y,width:cols*tutorialCell,height:rows*tutorialCell,fill:'none',class:'outer-edge'}));
return{
center:(row,col)=>[x+(col+.5)*tutorialCell,y+(row+.5)*tutorialCell],
gate:(side,row)=>side==='W'?[x,y+(row+.5)*tutorialCell]:[x+cols*tutorialCell,y+(row+.5)*tutorialCell]
};
}
function addGate(svg,point,side){
svg.append(
svgEl('circle',{cx:point[0],cy:point[1],r:4.5,class:'gate-dot',fill:'#72e38f'}),
svgEl('path',{d:gateMarkerPathAt(point,side,false,.82),class:'gate-marker tutorial-gate-marker'})
);
}
function addPath(svg,points,color,className){
const line=svgEl('polyline',{points:points.map(point=>point.join(',')).join(' '),class:`path tutorial-path ${className}`,stroke:color,pathLength:1});
svg.append(line);return line;
}
for(const host of document.querySelectorAll('[data-tutorial]')){
const type=host.dataset.tutorial,fill=type==='fill',
width=tutorialPad*2+tutorialCell*4,
height=tutorialPad*2+tutorialCell*3,
svg=svgEl('svg',{viewBox:`0 0 ${width} ${height}`,class:'tutorial-board','aria-hidden':'true'});
svg.style.setProperty('--line-width','8px');
if(fill){
const grid=addGrid(svg,{bg:levelBackground(4)[0]});
for(let row=0;row<3;row++){
const fromLeft=row!==1,start=grid.gate(fromLeft?'W':'E',row),end=grid.gate(fromLeft?'E':'W',row),
cells=fromLeft?[0,1,2,3]:[3,2,1,0],points=[start,...cells.map(col=>grid.center(row,col)),end];
addGate(svg,start,fromLeft?'W':'E');addGate(svg,end,fromLeft?'E':'W');
addPath(svg,points,LINE_COLORS[[2,0,3][row]],`tutorial-fill-path tutorial-fill-${row+1}`);
}
}else{
const grid=addGrid(svg),startRow=type==='draw'?0:1,endRow=type==='draw'?2:type==='bend'?0:1,
start=grid.gate('W',startRow),end=grid.gate('E',endRow);
addGate(svg,start,'W');addGate(svg,end,'E');
if(type==='draw'){
addPath(svg,[start,grid.center(0,0),grid.center(0,1),grid.center(1,1),grid.center(1,2),grid.center(2,2),grid.center(2,3),end],LINE_COLORS[2],'tutorial-draw-path');
}else if(type==='bend'){
const numberCell=grid.center(1,1),number=svgEl('text',{x:numberCell[0],y:numberCell[1],class:'num tutorial-number-pulse'});
addPath(svg,[start,grid.center(1,0),numberCell,grid.center(0,1),grid.center(0,2),grid.center(0,3),end],LINE_COLORS[3],'tutorial-bend-path');
number.textContent='2';svg.append(number);
}else{
const points=[start,grid.center(1,0),grid.center(1,1),grid.center(1,2),grid.center(1,3),end];
addPath(svg,points,LINE_COLORS[2],'tutorial-shop-path');
const markerPoint=grid.center(1,2),marker=svgEl('g',{class:'tutorial-shop-marker'});
marker.append(svgEl('rect',{x:markerPoint[0]-19,y:markerPoint[1]-17,width:38,height:34,class:'tutorial-shop-building'}),svgEl('text',{x:markerPoint[0],y:markerPoint[1]-1,class:'tutorial-shop-gem'}),svgEl('text',{x:markerPoint[0],y:markerPoint[1]+12,class:'tutorial-shop-label'}));
marker.children[1].textContent='\u25a6';marker.children[2].textContent='\u5e97';svg.append(marker);
const score=svgEl('text',{x:markerPoint[0]+31,y:markerPoint[1]-19,class:'tutorial-shop-score'});score.textContent='+ ◆';svg.append(score);
}
}
host.replaceChildren(svg);
}
}
buildTutorialBoards();
function boardCellsPath(cells){return(cells||[]).map(([r,c])=>{const x=PAD+c*CELL,y=PAD+r*CELL;return`M${x} ${y}h${CELL}v${CELL}h-${CELL}Z`}).join('')}
function outerEdgesPath(p){const set=cellSet(p),parts=[];for(const[r,c]of p.valid){const x=PAD+c*CELL,y=PAD+r*CELL;if(!set.has(ckey(r-1,c)))parts.push(`M${x} ${y}h${CELL}`);if(!set.has(ckey(r+1,c)))parts.push(`M${x} ${y+CELL}h${CELL}`);if(!set.has(ckey(r,c-1)))parts.push(`M${x} ${y}v${CELL}`);if(!set.has(ckey(r,c+1)))parts.push(`M${x+CELL} ${y}v${CELL}`)}return parts.join('')}
function drawOuterEdges(layer,p){layer.append(svgEl('path',{d:outerEdgesPath(p),class:'outer-edge'}))}
function overviewChunkPath(meta){return meta.chunks.map(([dx,dy])=>{const x=PAD+dx*UNIT,y=PAD+dy*UNIT;return`M${x} ${y}h${UNIT}v${UNIT}h-${UNIT}Z`}).join('')}
function boardTimeout(board,callback,delay){
const timer=setTimeout(()=>{board?.timers?.delete(timer);if(board?.card?.isConnected)callback()},delay);board?.timers?.add(timer);return timer;
}
function makeBoard(meta){
const makeStarted=perfStart();
const p=puzzleOf(meta),w=p.bounds.w*CELL+PAD*2,h=p.bounds.h*CELL+PAD*2;
const card=document.createElement('div');card.className='board-card';card.style.left=((meta.x-renderOriginX)*UNIT-PAD)+'px';card.style.top=((meta.y-renderOriginY)*UNIT-PAD)+'px';card.style.width=w+'px';card.style.height=h+'px';card.dataset.id=meta.id;
const region=regionFor(meta.targetLevel??meta.level);card.dataset.region=region.name.toLowerCase();card.dataset.difficulty=String(meta.level);card.style.setProperty('--region',region.accent);
const label=document.createElement('div');label.className='board-label';label.hidden=true;label.dataset.boardId=meta.id;label.style.setProperty('--region',region.accent);
const solverBadge=document.createElement('div');solverBadge.className='solver-badge';solverBadge.setAttribute('aria-hidden','true');
const claimBadge=document.createElement('div');claimBadge.className='board-claim-badge';claimBadge.hidden=true;claimBadge.setAttribute('aria-live','polite');
const solverCaption=document.createElement('span');solverCaption.textContent='\u30af\u30ea\u30a2';
const solverName=document.createElement('strong');solverBadge.append(solverCaption,solverName);
const boardActions=document.createElement('div');boardActions.className='board-actions';
const boardReset=document.createElement('button');boardReset.type='button';boardReset.className='board-action board-reset';boardReset.textContent='\u21ba \u30ea\u30bb\u30c3\u30c8';boardReset.setAttribute('aria-label','\u3053\u306e\u76e4\u9762\u306e\u7dda\u3092\u3059\u3079\u3066\u6d88\u3059');boardActions.append(boardReset);label.append(boardActions);
const storeButton=document.createElement('button');storeButton.type='button';storeButton.className='line-store';storeButton.hidden=true;storeButton.innerHTML='<span class="shop-shell" aria-hidden="true"><i></i><b></b><em></em></span><small>ショップ</small>';
const scoreLensBadge=document.createElement('div');scoreLensBadge.className='score-lens-badge';scoreLensBadge.hidden=true;scoreLensBadge.setAttribute('aria-hidden','true');
const svg=svgEl('svg',{viewBox:`0 0 ${w} ${h}`,width:w,height:h,class:'board-svg',tabindex:0,role:'group','aria-label':`\u30ec\u30d9\u30eb${meta.level}`});
const overviewLayer=svgEl('g',{class:'overview-layer'}),staticLayer=svgEl('g',{class:'static-layer'}),specialLayer=svgEl('g',{class:'special-cell-layer'}),connectorLayer=svgEl('g',{class:'connector-layer'}),pathLayer=svgEl('g',{class:'path-layer'}),dragLayer=svgEl('g',{class:'drag-layer'}),claimPreviewLayer=svgEl('g',{class:'claim-preview-layer'}),numberLayer=svgEl('g',{class:'number-layer'}),gateLayer=svgEl('g',{class:'gate-layer'});
overviewLayer.append(svgEl('path',{d:overviewChunkPath(meta),class:'overview-fill'}));
const[bgStart,bgEnd]=levelBackground(meta.level),bgId=`board-level-bg-${meta.id}`,bgDefs=svgEl('defs'),bgGradient=svgEl('linearGradient',{id:bgId,x1:'0%',y1:'0%',x2:'100%',y2:'100%'});
bgGradient.append(svgEl('stop',{offset:'0%','stop-color':bgStart}),svgEl('stop',{offset:'100%','stop-color':bgEnd}));bgDefs.append(bgGradient);staticLayer.append(bgDefs);
const validCellPath=boardCellsPath(p.valid),inputClipId=`board-input-clip-${meta.id}`,inputClip=svgEl('clipPath',{id:inputClipId,clipPathUnits:'userSpaceOnUse'}),inputClipShape=svgEl('path',{d:validCellPath});inputClip.append(inputClipShape);bgDefs.append(inputClip);
const cellShape=svgEl('path',{d:validCellPath,class:'cell-shape',fill:`url(#${bgId})`}),unfilledWarningShape=svgEl('path',{d:'',class:'unfilled-warning-cells'}),boardInputSurface=svgEl('path',{d:validCellPath,class:'board-input-surface','aria-hidden':'true'});staticLayer.append(cellShape,unfilledWarningShape,boardInputSurface)
const obstacleNodes=new Map();for(const[r,c]of p.obstacles||[]){const x=PAD+c*CELL,y=PAD+r*CELL,group=svgEl('g',{class:'obstacle-mark'});obstacleNodes.set(ckey(r,c),group);group.append(svgEl('rect',{x,y,width:CELL,height:CELL,class:'obstacle-cell'}),svgEl('line',{x1:x+10,y1:y+10,x2:x+CELL-10,y2:y+CELL-10,class:'obstacle-cross'}),svgEl('line',{x1:x+CELL-10,y1:y+10,x2:x+10,y2:y+CELL-10,class:'obstacle-cross'}));staticLayer.append(group)}
drawOuterEdges(staticLayer,p);
const specialInfo=specialCellInfoMap(p),specialSets=specialCellSet(p),specialNodes=new Map();
for(const[key,info]of specialInfo){const[r,c]=key.split(',').map(Number),marker=makeSpecialMarker(info,r,c);specialNodes.set(key,marker);specialLayer.append(marker)}
const numberNodes=new Map(),numberWarningNodes=new Map();for(const[r,c,v]of p.n){
const[x,y]=boardCellCenter([r,c]),key=ckey(r,c),node=svgEl('text',{x,y,class:'num'}),
warning=svgEl('g',{class:'number-turn-warning','aria-hidden':'true'}),
frame=svgEl('rect',{x:x-CELL*.39,y:y-CELL*.39,width:CELL*.78,height:CELL*.78,class:'number-warning-frame'}),
caption=svgEl('text',{x,y:y+14,class:'number-warning-caption'});
node.textContent=v;caption.textContent='\u66f2\u304c\u308b';warning.append(frame,caption);numberNodes.set(key,node);numberWarningNodes.set(key,warning);numberLayer.append(node,warning);
}
const gateDots=[],gateKnobs=[],gateMarkers=[],gateHits=[],gateIndexesByCell=new Map();
p.g.forEach((_,i)=>{
const g=gateObj(p,i),gp=gatePoint(g),sealed=!gateConnectionAllowed(meta,i),internalClass=g.internal?' internal':'',
dot=svgEl('circle',{cx:gp[0],cy:gp[1],r:g.internal?5.5:4.5,class:`gate-dot${internalClass}${sealed?' sealed':''}`,fill:'#7d878e'}),
knob=svgEl('circle',{cx:gp[0],cy:gp[1],r:g.internal?9:7,class:`gate-knob${internalClass}`,'data-gate-knob':i}),
marker=svgEl('path',{d:gateMarkerPathAt(gp,g.side,sealed,1,g.internal),class:`gate-marker${internalClass}${sealed?' sealed':''}`}),
hit=svgEl('rect',{...gateHitBox(gp,g.side,g.internal),rx:3,class:`gate-hit${internalClass}`,'clip-path':`url(#${inputClipId})`,'data-gate':i,tabindex:0,role:'button','aria-label':g.internal?`盤面内ゲート ${i+1}`:`${sealed?'終端':'ゲート '+(i+1)}`});
gateDots.push(dot);gateKnobs.push(knob);gateMarkers.push(marker);gateHits.push(hit);const gateCellKey=ckey(...g.cell),cellGates=gateIndexesByCell.get(gateCellKey)||[];cellGates.push(i);gateIndexesByCell.set(gateCellKey,cellGates);
gateLayer.append(marker,dot,knob,hit);
});
svg.append(overviewLayer,staticLayer,specialLayer,connectorLayer,gateLayer,pathLayer,dragLayer,claimPreviewLayer,numberLayer);card.append(svg,solverBadge,claimBadge,storeButton,scoreLensBadge);world.append(card);boardHudLayer?.append(label);
const b={id:meta.id,meta,p,card,svg,label,solverBadge,solverName,claimBadge,storeButton,drawing:null,pendingClaimPointer:null,pendingClaimFrame:0,armedGate:null,w,h,connectorLayer,pathLayer,dragLayer,claimPreviewLayer,numberLayer,specialLayer,dragPathIndex:null,pathStrokeNodes:[],pathVisualNodes:new Map(),connectorNodes:[],numberNodes,numberWarningNodes,numberWarningKeys:new Set(),multipleWarningKeys:new Set(),gateDots,gateKnobs,gateMarkers,gateHits,gateIndexesByCell,endpointIndexesByCell:new Map(),gateLayer,boardActions,boardReset,scoreLensBadge,cellShape,boardInputSurface,inputClipId,unfilledWarningShape,obstacleNodes,cellFlashAnimations:new Map(),specialNodes,specialInfo,specialSets,crossingKeySet:new Set(specialSets.crossings.map(cell=>ckey(...cell))),dragSpecialRevision:0,dragSpecialAppliedRevision:-1,unfilledWarningActive:false,solvedPathsRendered:false,specialCrossTriggered:false,timers:new Set()};
boardReset.addEventListener('click',event=>{event.preventDefault();event.stopPropagation();selectBoard(b);playSound('reset');void resetSelectedBoard(b)});
positionBoardLabel(b);rendered.set(meta.id,b);bindBoard(b);renderBoardNow(b);
if(meta.justRevealed){card.classList.add('revealing');boardTimeout(b,()=>card.classList.remove('revealing'),900);delete meta.justRevealed}
perfCount('boardsCreated');perfEnd('makeBoard',makeStarted);return b;
}
function destroyBoard(board){
if(!board)return;finishCompletionVisual(board.id,true);const pointerId=board.drawing?.pointerId;board.drawing=null;cancelBoardDragFrame(board);dirtyBoards.delete(board);for(const key of[...lineWidthDirtyPathKeys])if(key.startsWith(`${board.id}:`))lineWidthDirtyPathKeys.delete(key);
if(pointerId!=null)safeRelease(board.svg,pointerId);
for(const timer of board.timers||[])clearTimeout(timer);board.timers?.clear?.();
for(const animation of board.cellFlashAnimations?.values?.()||[])try{animation.cancel()}catch(_){}
releaseBoardAuroraPathCount(board);board.cellFlashAnimations?.clear?.();board.pathStrokeNodes=[];board.connectorNodes=[];board.pathVisualNodes?.clear?.();board.numberNodes?.clear?.();board.numberWarningNodes?.clear?.();board.specialNodes?.clear?.();board.dragLayer?.replaceChildren?.();board.pathLayer?.replaceChildren?.();board.label?.remove?.();board.card.remove();rendered.delete(board.id);perfCount('boardsDestroyed');
}
function pointerPointForDrawing(drawing,point){const offset=drawing?.pointerOffset||[0,0];return[point[0]+offset[0],point[1]+offset[1]]}
const POINTER_DIRECTION_DELTAS=Object.freeze({N:Object.freeze([-1,0]),S:Object.freeze([1,0]),W:Object.freeze([0,-1]),E:Object.freeze([0,1])});
function setPoint(target,x,y){if(Array.isArray(target)){target[0]=x;target[1]=y;return target}return[x,y]}
function setSvgAttr(node,name,value){const text=String(value);if(node.getAttribute(name)!==text)node.setAttribute(name,text)}
function drawingForPath(b,pathIndex,pointerId=null,lastPoint=null){
const path=metaState(b.id).paths[pathIndex],tip=path?.cells?.[path.cells.length-1]||[0,0],center=boardCellCenter(tip);
return{
phase:'dragging',pointerId,keyboardActive:pointerId==null,pathIndex,lastPoint,
rawPointerPosition:lastPoint?[lastPoint[0],lastPoint[1]]:null,currentConfirmedCell:[tip[0],tip[1]],
candidateDirection:null,currentCandidateCell:null,
renderedHandlePosition:[center[0],center[1]],originGate:null,originGateCell:null,gateSnapArmed:false,leftOriginGateCell:false,
warpDirection:null,segmentOccupancy:occupiedMap(metaState(b.id)),pathCellIndex:new Map((path?.cells||[]).map((cell,index)=>[ckey(...cell),index])),logicalRevision:0,lastModelProbeKey:null,lastRenderedTip:null
};
}
function syncDrawingConfirmedState(b,drawing=b?.drawing){
const path=drawing&&metaState(b.id).paths[drawing.pathIndex],tip=path?.cells?.[path.cells.length-1];if(!tip)return false;
drawing.currentConfirmedCell=setPoint(drawing.currentConfirmedCell,tip[0],tip[1]);
const center=boardCellCenter(tip);drawing.renderedHandlePosition=setPoint(drawing.renderedHandlePosition,center[0],center[1]);return true;
}
function clearDrawingPointerState(drawing){
if(!drawing)return;drawing.rawPointerPosition=null;drawing.candidateDirection=null;drawing.currentCandidateCell=null;
}
function directionFromDelta(dx,dy,preferredAxis=null){
const adx=Math.abs(dx),ady=Math.abs(dy),largest=Math.max(adx,ady);
if(largest<POINTER_SNAP_THRESHOLD)return null;
const nearTie=Math.abs(adx-ady)<CELL*.12,horizontal=nearTie?preferredAxis!=='V':adx>ady;
return horizontal?(dx<0?'W':'E'):(dy<0?'N':'S');
}
function updateDrawingPointerState(b,drawing,point){
if(!drawing||!point||!syncDrawingConfirmedState(b,drawing))return null;
drawing.rawPointerPosition=setPoint(drawing.rawPointerPosition,point[0],point[1]);
const center=boardCellCenter(drawing.currentConfirmedCell),dx=point[0]-center[0],dy=point[1]-center[1],direction=directionFromDelta(dx,dy,pathTailAxis(activePath(b),b.p));
drawing.candidateDirection=direction||null;
if(direction){
const delta=POINTER_DIRECTION_DELTAS[direction],cell=drawing.currentConfirmedCell;
drawing.currentCandidateCell=setPoint(drawing.currentCandidateCell,cell[0]+delta[0],cell[1]+delta[1]);
}else drawing.currentCandidateCell=null;
updateDrawingHandlePosition(b,drawing,point);
return direction;
}
function updateDrawingHandlePosition(b,drawing,point){
if(!drawing||!point||!syncDrawingConfirmedState(b,drawing))return false;
drawing.renderedHandlePosition=setPoint(drawing.renderedHandlePosition,point[0],point[1]);
return true;
}
function liveEndpointPoint(b,pathIndex,actualTip){
const drawing=b.drawing;
if(!drawing||drawing.pointerId==null||drawing.pathIndex!==pathIndex)return actualTip;
return drawing.renderedHandlePosition||actualTip;
}
function clearDragRender(b){
if(!b)return;
if(Number.isInteger(b.dragPathIndex))for(const node of b.pathVisualNodes?.get(b.dragPathIndex)||[])node.style.removeProperty('display');
if(Number.isInteger(b.dragGateIndex))b.gateKnobs?.[b.dragGateIndex]?.classList.remove('drag-target');
if(b.dragLayer)b.dragLayer.style.display='none';if(b.dragCache){b.dragCache.logicalRevision=-1;b.dragCache.geometryRevision=-1}b.dragPathIndex=null;b.dragGateIndex=null;b.dragSpecialAppliedRevision=-1;
}
function buildDragCache(b,index,blended,count){
const layer=b.dragLayer,defs=svgEl('defs'),strokes=[],gradients=[];layer.replaceChildren(defs);
for(let pieceIndex=0;pieceIndex<count;pieceIndex++){
let stroke=LINE_COLORS[0];
if(blended){
const id=`drag-gradient-${b.id}-${index}-${pieceIndex}`,gradient=svgEl('linearGradient',{id,gradientUnits:'userSpaceOnUse'}),start=svgEl('stop',{offset:'0%'}),end=svgEl('stop',{offset:'100%'});
gradient.append(start,end);defs.append(gradient);gradients.push({gradient,start,end});stroke=`url(#${id})`;
}
const node=svgEl('polyline',{class:'path drag-path',stroke});strokes.push(node);layer.append(node);
}
const liveTail=svgEl('line',{class:'path drag-path drag-live-tail'}),startHalo=svgEl('circle',{r:7,class:'endpoint-halo'}),startKnob=svgEl('circle',{r:4.8,class:'endpoint-knob'}),
matchOrbit=svgEl('g',{class:'number-match-orbit'}),matchOrbitRing=svgEl('circle',{r:12.5,class:'number-match-orbit-ring'}),
matchOrbitHoles=svgEl('circle',{r:12.5,class:'number-match-orbit-holes'}),halo=svgEl('circle',{r:7,class:'endpoint-halo'}),
knob=svgEl('circle',{r:4.8,class:'endpoint-knob'}),cursorClipId=`drag-cursor-clip-${b.id}-${index}`,
cursorClip=svgEl('clipPath',{id:cursorClipId}),cursorClipCircle=svgEl('circle',{cx:0,cy:0,r:DRAG_FLAG_CLIP_RADIUS}),
cursorImage=svgEl('image',{x:-DRAG_FLAG_CURSOR_SIZE/2,y:-DRAG_FLAG_CURSOR_SIZE/2,width:DRAG_FLAG_CURSOR_SIZE,height:DRAG_FLAG_CURSOR_SIZE,class:'endpoint-cursor-image','clip-path':`url(#${cursorClipId})`,preserveAspectRatio:'xMidYMid slice'}),
cursorEmoji=svgEl('text',{x:0,y:0,class:'endpoint-cursor-emoji'}),badge=svgEl('circle',{cx:19,cy:-19,r:10.5,class:'turn-badge'}),turns=svgEl('text',{x:19,y:-18.5,class:'turn-count'}),tipGroup=svgEl('g',{class:'drag-tip-group'});
cursorImage.style.display='none';cursorEmoji.style.display='none';cursorClip.append(cursorClipCircle);defs.append(cursorClip);
matchOrbit.append(matchOrbitRing,matchOrbitHoles);startHalo.style.display='none';startKnob.style.display='none';tipGroup.append(matchOrbit,halo,knob,cursorImage,cursorEmoji,badge,turns);
layer.append(liveTail,startHalo,startKnob,tipGroup);return b.dragCache={index,blended,count,strokes,gradients,logicalRevision:-1,geometryRevision:-1,startHalo,startKnob,liveTail,tipGroup,matchOrbit,matchOrbitRing,matchOrbitHoles,halo,knob,cursorImage,cursorEmoji,badge,turns};
}
function activeCustomCursorItem(){return cursorModel.item(data.cursorStyle)}
function updateDragCursorDesign(cache){
const item=activeCustomCursorItem(),showFlag=Boolean(item?.flagAsset),showEmoji=Boolean(item?.cursorEmoji&&!showFlag);
if(cache.cursorDesignStyle!==(item?.cursorStyle||null)){
cache.cursorDesignStyle=item?.cursorStyle||null;cache.cursorImage.style.display=showFlag?'':'none';cache.cursorEmoji.style.display=showEmoji?'':'none';
if(showFlag)cache.cursorImage.setAttribute('href',item.flagAsset);
if(showEmoji)cache.cursorEmoji.textContent=item.cursorEmoji;
}
}
function refreshDragSpecialState(b){
if(b.dragSpecialAppliedRevision===b.dragSpecialRevision)return;b.dragSpecialAppliedRevision=b.dragSpecialRevision;
const st=metaState(b.id),occupiedKeys=b.drawing?.segmentOccupancy||occupiedMap(st);
const special=b.specialSets;
for(const cell of special.crossings)b.specialNodes.get(ckey(...cell))?.classList.toggle('activated',Boolean(crossingStateAtCell(st,b.p,cell)));
for(const pair of special.warps)for(const cell of[pair.a,pair.b])b.specialNodes.get(ckey(...cell))?.classList.toggle('activated',occupiedKeys.has(ckey(...cell)));
for(const lock of special.locks){const keyTouched=st.paths.some(path=>path.cells.some(cell=>sameCell(cell,lock.key))),doorUnlocked=st.paths.some(path=>pathHasLockKey(path,lock));b.specialNodes.get(ckey(...lock.key))?.classList.toggle('activated',keyTouched);b.specialNodes.get(ckey(...lock.door))?.classList.toggle('unlocked',doorUnlocked)}
}
function matchingNumberKeys(path,p){
if(!path?.cells?.length)return new Set();
const turns=partialTurnCount(path,p),occupied=new Set(path.cells.map(cell=>ckey(...cell)));
return new Set(p.n.filter(([r,c,value])=>value===turns&&occupied.has(ckey(r,c))).map(([r,c])=>ckey(r,c)));
}
function showNumberMatchEffect(b,key,cache){
const cell=key.split(',').map(Number),[cx,cy]=boardCellCenter(cell),pulse=svgEl('circle',{cx,cy,r:9,class:'number-match-pulse'});
b.numberLayer.append(pulse);boardTimeout(b,()=>pulse.remove(),420);
}
function updateNumberMatchFeedback(b,path,cache){
const drawing=b?.drawing;if(!drawing)return;
const current=matchingNumberKeys(path,b.p),previous=drawing.matchingNumberKeys;
if(previous)for(const key of current)if(!previous.has(key))showNumberMatchEffect(b,key,cache);
cache.knob.classList.toggle('number-match',current.size>0);cache.halo.classList.toggle('number-match',current.size>0);cache.matchOrbit.classList.toggle('show',current.size>0);
drawing.matchingNumberKeys=current;
}
function drawingLineWidth(b,pathIndex){const drawing=b?.drawing;if(drawing?.pathIndex===pathIndex&&Number.isFinite(drawing.lineWidth))return drawing.lineWidth;const width=renderedConnectedLineWidth(b.meta,pathIndex);if(drawing?.pathIndex===pathIndex)drawing.lineWidth=width;return width}
function renderDragFrame(b){
const started=perfStart(),drawing=b?.drawing,path=activePath(b);if(!drawing||drawing.pointerId==null||!path?.cells?.length){clearDragRender(b);return false}
if(!drawing.pickupStartMeasured){
drawing.pickupStartMeasured=true;perfGauge('pickupStartFullBoardRenders',Math.max(0,(perfCounters.fullBoardRenders||0)-(drawing.fullBoardRendersAtPointerDown||0)));
if(Number.isFinite(drawing.pointerDownAt))perfObserve('pickupStartLatency',Math.max(0,perfNow()-drawing.pointerDownAt));
}
dirtyBoards.delete(b);
const index=drawing.pathIndex,baseNodes=b.pathVisualNodes?.get(index)||[];if(!baseNodes.length&&!drawing.createdPath){renderBoardNow(b);return false}
if(b.dragPathIndex!==index){clearDragRender(b);b.dragPathIndex=index;for(const node of baseNodes)node.style.display='none'}
b.dragLayer.style.display='';
const logicalRevision=drawing.logicalRevision||0,startIndex=path.startColorIndex??path.colorIndex??index,endIndex=path.endColorIndex??startIndex,
startColor=LINE_COLORS[startIndex%LINE_COLORS.length],endColor=LINE_COLORS[endIndex%LINE_COLORS.length],
blended=false,lineWidth=drawingLineWidth(b,index);
if(drawing.renderGeometryRevision!==logicalRevision||drawing.renderBlended!==blended){
drawing.renderGeometryRevision=logicalRevision;drawing.renderBlended=blended;drawing.renderSegments=pathRenderSegments(path,b.p);drawing.renderTurnCount=String(partialTurnCount(path,b.p));
drawing.renderPieces=blended?pathStrokePieces(drawing.renderSegments,startColor,endColor):drawing.renderSegments.map(points=>({points}));
}
const pieces=drawing.renderPieces||[],cache=!b.dragCache||b.dragCache.index!==index||b.dragCache.blended!==blended||b.dragCache.count!==pieces.length?buildDragCache(b,index,blended,pieces.length):b.dragCache,
effectClass=lineEffectClass(path.lineEffect);
for(const node of cache.strokes)node.setAttribute('class',`path drag-path${effectClass}`);cache.liveTail.setAttribute('class',`path drag-path drag-live-tail${effectClass}`);for(const node of[cache.startHalo,cache.startKnob,cache.halo,cache.knob])node.classList.toggle('line-effect-aurora',path.lineEffect==='aurora');
const geometryChanged=cache.geometryRevision!==logicalRevision;
if(geometryChanged){
pieces.forEach((piece,pieceIndex)=>{
const node=cache.strokes[pieceIndex],points=piece.points,pointsText=points.map(point=>point.join(',')).join(' ');if(node.getAttribute('points')!==pointsText)node.setAttribute('points',pointsText);applyLineWidth(node,lineWidth);
if(blended){const first=points[0],last=points[points.length-1],gradient=cache.gradients[pieceIndex];gradient.gradient.setAttribute('x1',first[0]);gradient.gradient.setAttribute('y1',first[1]);gradient.gradient.setAttribute('x2',last[0]);gradient.gradient.setAttribute('y2',last[1]);gradient.start.setAttribute('stop-color',piece.startColor);gradient.end.setAttribute('stop-color',piece.endColor)}
else if(node.getAttribute('stroke')!==startColor)node.setAttribute('stroke',startColor);
});cache.geometryRevision=logicalRevision;
}
const actualTip=boardCellCenter(path.cells[path.cells.length-1]),tip=liveEndpointPoint(b,index,actualTip),color=startColor,turnCount=drawing.renderTurnCount||'0',colorChanged=drawing.lastRenderedColor!==color;
const previousTip=drawing.lastRenderedTip;if(previousTip&&!colorChanged&&cache.logicalRevision===logicalRevision&&Math.abs(previousTip[0]-tip[0])<.45&&Math.abs(previousTip[1]-tip[1])<.45){perfEnd('renderDragFrame',started);return true}drawing.lastRenderedColor=color;drawing.lastRenderedTip=setPoint(drawing.lastRenderedTip,tip[0],tip[1]);
const tailNow=perfNow(),tailDue=geometryChanged||!drawing.lastTailRenderAt||tailNow-drawing.lastTailRenderAt>=32;
if(tailDue){drawing.lastTailRenderAt=tailNow;setSvgAttr(cache.liveTail,'x1',actualTip[0]);setSvgAttr(cache.liveTail,'y1',actualTip[1]);setSvgAttr(cache.liveTail,'x2',tip[0]);setSvgAttr(cache.liveTail,'y2',tip[1]);setSvgAttr(cache.liveTail,'stroke',startColor);applyLineWidth(cache.liveTail,lineWidth)}
if(path.detachedStart){const startTip=boardCellCenter(path.cells[0]);for(const node of[cache.startHalo,cache.startKnob]){node.style.display='';node.setAttribute('cx',startTip[0]);node.setAttribute('cy',startTip[1])}cache.startKnob.setAttribute('fill',startColor)}else for(const node of[cache.startHalo,cache.startKnob])node.style.display='none';
const tipTransform=`translate3d(${tip[0]}px,${tip[1]}px,0)`;if(cache.tipGroup.style.transform!==tipTransform)cache.tipGroup.style.transform=tipTransform;
setSvgAttr(cache.knob,'fill',color);if(data.cursorStyle!=='default')updateDragCursorDesign(cache);if(cache.turns.textContent!==turnCount)cache.turns.textContent=turnCount;refreshDragSpecialState(b);
if(cache.logicalRevision!==logicalRevision){updateNumberMatchFeedback(b,path,cache);refreshDragNumberColors(b);cache.logicalRevision=logicalRevision}
const gateIndex=gateAtCell(b.p,path.cells[path.cells.length-1],path.startGate),nextGateIndex=Number.isInteger(gateIndex)?gateIndex:null;
if(b.dragGateIndex!==nextGateIndex){if(Number.isInteger(b.dragGateIndex))b.gateKnobs[b.dragGateIndex]?.classList.remove('drag-target');b.dragGateIndex=nextGateIndex;if(Number.isInteger(nextGateIndex))b.gateKnobs[nextGateIndex]?.classList.add('drag-target')}
perfCount('dragRenders');perfEnd('renderDragFrame',started);return true;
}
function refreshDragNumberColors(b){
const st=metaState(b.id),drawing=b.drawing,path=drawing&&st.paths[drawing.pathIndex];if(!drawing||!path)return;
const current=new Set(path.cells.map(cell=>ckey(...cell)).filter(key=>b.numberNodes.has(key))),affected=new Set([...(drawing.dragNumberKeys||[]),...current]),occupancy=drawing.segmentOccupancy||occupiedMap(st);
for(const key of affected){
const node=b.numberNodes.get(key);if(!node)continue;const pathIndex=occupancy.get(key),owner=Number.isInteger(pathIndex)?st.paths[pathIndex]:null,cell=key.split(',').map(Number),lineColor=owner?pathColorAtCell(owner,b.p,cell):null;
node.classList.toggle('on-line',Boolean(lineColor));if(lineColor)node.style.setProperty('--number-line-color',lineColor);else node.style.removeProperty('--number-line-color');
}
drawing.dragNumberKeys=current;
}
function boardPlayHudVisible(b,solved=metaState(b.id).solved){return !solved&&hudBoardId===b?.id}
function setBoardHudVisibility(b,visible){if(!b)return false;const next=Boolean(visible);b.card.classList.toggle('hud-current',next);b.label?.classList.toggle('hud-visible',next);if(b.label)b.label.hidden=!next;if(next)positionBoardLabel(b);return next}
function activateBoardHud(b){
if(!b)return false;const previous=rendered.get(hudBoardId);hudBoardId=b.id;
if(previous&&previous!==b)setBoardHudVisibility(previous,false);
setBoardHudVisibility(b,boardPlayHudVisible(b));return true;
}
function renderBoardNow(b){
const renderStarted=perfStart();perfCount('fullBoardRenders');
clearDragRender(b);
const{p,meta,card,label,pathLayer,connectorLayer}=b,st=metaState(meta.id);
rebuildEndpointIndexes(b,st);
const solved=!!st.solved,validation=new Map(),lineLengthCache=currentLineGraphCaches().widths;
connectorLayer.replaceChildren();b.connectorNodes=[];card.classList.toggle('solved',solved);applyClaimPresentationToBoard(b);updateScoreLensBadge(b);const hudVisible=boardPlayHudVisible(b,solved);setBoardHudVisibility(b,hudVisible);b.boardActions.hidden=solved;
const solver=st.solvedBy&&st.solvedBy!==LEGACY_LOCAL_SOLVER?st.solvedBy:currentPlayerName();
b.solverName.textContent=solver;b.solverBadge.setAttribute('aria-hidden',solved?'false':'true');
const store=solved?st.store:null,storeCell=store?storeCellForMeta(meta):null;
b.storeButton.hidden=!storeCell;for(const[key,node]of b.obstacleNodes||[])node.style.display=storeCell&&key===ckey(...storeCell)?'none':'';
if(storeCell){
const[x,y]=boardCellCenter(storeCell);b.storeButton.style.left=x+'px';b.storeButton.style.top=y+'px';
b.storeButton.setAttribute('aria-label',`${store.owner}\u306e\u30b7\u30e7\u30c3\u30d7`);
b.storeButton.title=`${store.owner}\u306e\u30b7\u30e7\u30c3\u30d7`;
}
card.tabIndex=solved?0:-1;if(solved)card.setAttribute('aria-label',`\u30ec\u30d9\u30eb${meta.level}\u3002\u30af\u30ea\u30a2\u6e08\u307f\u3002`);else card.removeAttribute('aria-label');
b.svg.setAttribute('tabindex',solved?-1:0);
if(!solved)for(const path of st.paths)if(path.endGate!=null)validation.set(path,pathValid(path,p));
const invalidPaths=solved?0:[...validation.values()].filter(valid=>!valid).length;
card.classList.toggle('has-errors',invalidPaths>0);b.hadInvalidPaths=invalidPaths>0;
const occupiedKeys=new Set();for(const route of st.paths)for(const cell of route?.cells||[])occupiedKeys.add(ckey(...cell));
const special=specialCellSet(p);
for(const cell of special.crossings)b.specialNodes.get(ckey(...cell))?.classList.toggle('activated',Boolean(crossingStateAtCell(st,p,cell)));
for(const pair of special.warps)for(const cell of[pair.a,pair.b])b.specialNodes.get(ckey(...cell))?.classList.toggle('activated',occupiedKeys.has(ckey(...cell)));
for(const lock of special.locks){
const keyTouched=st.paths.some(path=>path.cells.some(cell=>sameCell(cell,lock.key))),doorUnlocked=st.paths.some(path=>pathHasLockKey(path,lock));
b.specialNodes.get(ckey(...lock.key))?.classList.toggle('activated',keyTouched);b.specialNodes.get(ckey(...lock.door))?.classList.toggle('unlocked',doorUnlocked);
}
const allLinesConnected=st.paths.length===p.n.length&&st.paths.length>0&&st.paths.every(route=>route?.endGate!=null),emptyKeys=new Set(p.valid.map(cell=>ckey(...cell)).filter(key=>!occupiedKeys.has(key))),unfilledWarning=!solved&&allLinesConnected&&emptyKeys.size>0;
if(b.unfilledWarningShape)b.unfilledWarningShape.setAttribute('d',unfilledWarning?boardCellsPath(p.valid.filter(cell=>emptyKeys.has(ckey(...cell)))):'');
card.classList.toggle('has-unfilled-warning',unfilledWarning);
const showUnfilledWarning=unfilledWarning&&!b.unfilledWarningActive&&activeBoard===meta.id;
b.unfilledWarningActive=unfilledWarning;
label.replaceChildren(Object.assign(document.createElement('b'),{textContent:`\u30ec\u30d9\u30eb${meta.level}`}),b.boardActions);if(hudVisible)positionBoardLabel(b);
if(!solved||!b.solvedPathsRendered){
pathLayer.replaceChildren();b.pathStrokeNodes=[];b.pathVisualNodes=new Map();
const defs=svgEl('defs');pathLayer.append(defs);
st.paths.forEach((path,index)=>{
if(!path.cells.length)return;
const visualNodes=[];b.pathVisualNodes.set(index,visualNodes);
const pointerDragging=b.drawing?.pathIndex===index&&b.drawing.pointerId!=null,segments=pathRenderSegments(path,p),actualTip=boardCellCenter(path.cells[path.cells.length-1]),visualTip=pointerDragging?liveEndpointPoint(b,index,actualTip):actualTip;
if(pointerDragging&&path.endGate==null&&segments.length){const liveSegment=segments[segments.length-1];if(liveSegment.length)liveSegment[liveSegment.length-1]=visualTip}
const startIndex=path.startColorIndex??path.colorIndex??index,endIndex=path.endColorIndex??startIndex,colorEndGate=path.endGate??path.openGate,
startColor=LINE_COLORS[startIndex%LINE_COLORS.length],endColor=LINE_COLORS[endIndex%LINE_COLORS.length],invalid=path.endGate!=null&&!validation.get(path)&&!solved,
lineWidth=renderedConnectedLineWidth(meta,index,lineLengthCache),blended=!uiSettings.lightweightRendering&&colorEndGate!=null&&endIndex!==startIndex;
if(blended){
for(const[pieceIndex,piece]of pathStrokePieces(segments,startColor,endColor).entries()){
const first=piece.points[0],last=piece.points[piece.points.length-1],gradientId=`path-gradient-${b.id}-${index}-${pieceIndex}`,gradient=svgEl('linearGradient',{id:gradientId,gradientUnits:'userSpaceOnUse',x1:first[0],y1:first[1],x2:last[0],y2:last[1]});
gradient.append(svgEl('stop',{offset:'0%','stop-color':piece.startColor}),svgEl('stop',{offset:'100%','stop-color':piece.endColor}));defs.append(gradient);
const pathNode=svgEl('polyline',{points:piece.points.map(q=>q.join(',')).join(' '),class:`path${lineEffectClass(path.lineEffect)}${invalid?' invalid':''}`,stroke:`url(#${gradientId})`,'data-path-index':index,'data-path-progress-start':piece.startProgress.toFixed(6),'data-path-progress-end':piece.endProgress.toFixed(6)});
applyLineWidth(pathNode,lineWidth);b.pathStrokeNodes.push(pathNode);visualNodes.push(pathNode);pathLayer.append(pathNode);
}
}else for(const points of segments){
const pathNode=svgEl('polyline',{points:points.map(q=>q.join(',')).join(' '),class:`path${lineEffectClass(path.lineEffect)}${invalid?' invalid':''}`,stroke:startColor,'data-path-index':index});
applyLineWidth(pathNode,lineWidth);b.pathStrokeNodes.push(pathNode);visualNodes.push(pathNode);pathLayer.append(pathNode);
}
if(path.endGate==null){
const tip=visualTip,isActive=b.drawing?.pathIndex===index,turns=partialTurnCount(path,p),bx=tip[0]+19,by=tip[1]-19,endPickupColor=LINE_COLORS[displayedEndpointColorIndex(meta,path,'end')%LINE_COLORS.length],startPickupColor=LINE_COLORS[displayedEndpointColorIndex(meta,path,'start')%LINE_COLORS.length];
const
halo=svgEl('circle',{cx:tip[0],cy:tip[1],r:7,class:`endpoint-halo${lineEffectClass(path.lineEffect)}`}),knob=svgEl('circle',{cx:tip[0],cy:tip[1],r:4.8,class:`endpoint-knob${lineEffectClass(path.lineEffect)}`,fill:endPickupColor}),hitNode=svgEl('circle',{cx:tip[0],cy:tip[1],r:24,class:'endpoint-hit','clip-path':`url(#${b.inputClipId})`,'data-path-index':index,'data-endpoint-side':'end',tabindex:0,role:'button','aria-label':`\u7dda\u306e\u7d9a\u304d\u3092\u5f15\u304f: ${index+1}`});
if(path.detachedStart){const startTip=boardCellCenter(path.cells[0]),startHalo=svgEl('circle',{cx:startTip[0],cy:startTip[1],r:7,class:`endpoint-halo${lineEffectClass(path.lineEffect)}`}),startKnob=svgEl('circle',{cx:startTip[0],cy:startTip[1],r:4.8,class:`endpoint-knob${lineEffectClass(path.lineEffect)}`,fill:startPickupColor}),startHit=svgEl('circle',{cx:startTip[0],cy:startTip[1],r:24,class:'endpoint-hit','clip-path':`url(#${b.inputClipId})`,'data-path-index':index,'data-endpoint-side':'start',tabindex:0,role:'button','aria-label':`\u7dda\u306e\u53cd\u5bfe\u5074\u3092\u5f15\u304f: ${index+1}`});visualNodes.push(startHalo,startKnob,startHit);pathLayer.append(startHalo,startKnob,startHit)}
visualNodes.push(halo,knob,hitNode);pathLayer.append(halo,knob,hitNode);
if(isActive){const badge=svgEl('circle',{cx:bx,cy:by,r:10.5,class:'turn-badge'}),t=svgEl('text',{x:bx,y:by+.5,class:'turn-count'});t.textContent=turns;visualNodes.push(badge,t);pathLayer.append(badge,t)}
}
});
b.solvedPathsRendered=solved;
}
if(!solved)b.solvedPathsRendered=false;
p.g.forEach((_,gi)=>{
const hit=matchingNeighborGate(meta,gi);if(!hit)return;
const neighborState=metaState(hit.meta.id),own=gateUsedPath(st,gi),other=gateUsedPath(neighborState,hit.gateIndex);
if(!own&&!other)return;
const localColorIndex=confirmedBoundaryColorIndex(meta,gi)??pathColorIndexAtGate(own,gi)??pathColorIndexAtGate(other,hit.gateIndex)??0,
localColor=LINE_COLORS[localColorIndex%LINE_COLORS.length],
g=gateObj(p,gi),gp=gatePoint(g),[dr,dc]=SIDE_D[g.side],end=[gp[0]+dc*CELL*.5,gp[1]+dr*CELL*.5];
const connectorPath=own||other,connector=svgEl('line',{x1:gp[0],y1:gp[1],x2:end[0],y2:end[1],class:`path connector${lineEffectClass(connectorPath?.lineEffect)}`,stroke:localColor});
if(own){connector.dataset.ownerBoard=meta.id;connector.dataset.pathIndex=String(st.paths.indexOf(own))}
else{connector.dataset.ownerBoard=hit.meta.id;connector.dataset.pathIndex=String(neighborState.paths.indexOf(other))}
const owner=data.metas[connector.dataset.ownerBoard],ownerIndex=Number(connector.dataset.pathIndex);
if(owner&&Number.isInteger(ownerIndex)){const width=renderedConnectedLineWidth(owner,ownerIndex,lineLengthCache);applyLineWidth(connector,width)}
b.connectorNodes.push(connector);connectorLayer.append(connector);
});
if(!solved){
const bad=new Set(),turnWarnings=straightNumberWarningKeys(st,p),multipleWarnings=multipleNumberWarningKeys(st,p),
warningKeys=new Set([...turnWarnings,...multipleWarnings]);st.paths.forEach(path=>{if(path.endGate==null)return;const nums=numbersForPath(path,p),ta=turnAnalysis(path,p);for(const num of nums)if(nums.length!==1||num.value!==ta.count||!ta.cells.some(c=>sameCell(c,num.cell)))bad.add(ckey(...num.cell))});
const numberOccupancy=occupiedMap(st);
for(const[key,node]of b.numberNodes){
const multiple=multipleWarnings.has(key),warning=b.numberWarningNodes.get(key),pathIndex=numberOccupancy.get(key),path=Number.isInteger(pathIndex)?st.paths[pathIndex]:null,
cell=key.split(',').map(Number),lineColor=path?pathColorAtCell(path,p,cell):null;
node.classList.toggle('bad',bad.has(key));node.classList.toggle('turn-warning',warningKeys.has(key));node.classList.toggle('multiple-warning',multiple);node.classList.toggle('on-line',Boolean(lineColor));
if(lineColor)node.style.setProperty('--number-line-color',lineColor);else node.style.removeProperty('--number-line-color');
warning?.classList.toggle('show',warningKeys.has(key));warning?.classList.toggle('multiple',multiple);
const caption=warning?.querySelector('.number-warning-caption');if(caption)caption.textContent=multiple?'1\u7dda1\u500b':'\u66f2\u304c\u308b';
}
if(activeBoard===meta.id&&[...multipleWarnings].some(key=>!b.multipleWarningKeys.has(key)))toast('\u6570\u5b57\u306f1\u672c\u306e\u7dda\u306b1\u3064\u3067\u3059\u3002');
else if(activeBoard===meta.id&&[...turnWarnings].some(key=>!b.numberWarningKeys.has(key)))toast('\u6570\u5b57\u306e\u30de\u30b9\u3067\u3001\u8868\u793a\u56de\u6570\u3068\u66f2\u304c\u308a\u6570\u3092\u5408\u308f\u305b\u307e\u3059\u3002');
b.numberWarningKeys=turnWarnings;b.multipleWarningKeys=multipleWarnings;
if(showUnfilledWarning)toast('\u307e\u3060\u7a7a\u3044\u3066\u3044\u308b\u30de\u30b9\u304c\u3042\u308a\u307e\u3059\u3002');
}
const gateOwnColors=new Map();
st.paths.forEach(path=>{
if(!path.detachedStart)gateOwnColors.set(path.startGate,LINE_COLORS[(pathColorIndexAtGate(path,path.startGate)??0)%LINE_COLORS.length]);
if(path.endGate!=null)gateOwnColors.set(path.endGate,LINE_COLORS[(pathColorIndexAtGate(path,path.endGate)??0)%LINE_COLORS.length]);
if(path.openGate!=null)gateOwnColors.set(path.openGate,LINE_COLORS[(pathColorIndexAtGate(path,path.openGate)??0)%LINE_COLORS.length]);
});
p.g.forEach((g,i)=>{
const inherited=sharedBoundaryColorIndex(meta,i),path=gateUsedPath(st,i),
color=inherited!=null?LINE_COLORS[inherited]:(gateOwnColors.get(i)||'#f4f5f5'),
sealed=!gateConnectionAllowed(meta,i),[gr,gc]=globalCell(meta,[g[0],g[1]]),[dr,dc]=SIDE_D[g[2]],
frontier=!isInternalGateIndex(p,i)&&!sealed&&!matchingNeighborGate(meta,i)&&!unitOccupiedAtGlobalCell(gr+dr,gc+dc,meta.id),
tip=path&&path.endGate==null&&path.cells.length?path.cells[path.cells.length-1]:null,
connected=Boolean(path&&path.endGate!=null),
armed=b.armedGate===i||path?.openGate===i||(path&&path.endGate==null&&sameCell(tip,[g[0],g[1]])),
gate=gateObj(p,i),gp=gatePoint(gate),gateCellKey=ckey(g[0],g[1]),available=!solved&&!path&&!occupiedKeys.has(gateCellKey),auroraGate=!sealed&&(path?.lineEffect==='aurora'||available&&activeLineColorItem()?.aurora===true);
b.gateDots[i].setAttribute('fill',color);b.gateDots[i].classList.toggle('line-effect-aurora',auroraGate);
b.gateDots[i].classList.toggle('sealed',sealed);
b.gateDots[i].classList.toggle('frontier',frontier);
b.gateKnobs[i]?.setAttribute('fill',color);b.gateKnobs[i]?.classList.toggle('line-effect-aurora',auroraGate);
b.gateKnobs[i]?.classList.toggle('connected',connected&&!sealed);
b.gateKnobs[i]?.classList.toggle('show',(Boolean(armed)||connected||available||Boolean(path?.openGate===i))&&!sealed);
b.gateMarkers[i]?.setAttribute('d',gateMarkerPathAt(gp,gate.side,sealed,1,gate.internal));
b.gateMarkers[i]?.setAttribute('stroke',sealed?'#7d878e':color);b.gateMarkers[i]?.classList.toggle('line-effect-aurora',auroraGate);
b.gateMarkers[i]?.classList.toggle('sealed',sealed);
});
// Pointer dragging redraws the active polyline every frame. Its width was
// already assigned synchronously above, so defer whole-world work until the
if(b.drawing?.pointerId==null)queueLineWidthRefresh(b.id);else renderDragFrame(b);
updateBoardAuroraPathCount(b);
if(minimapDirty)scheduleMinimap();if(overviewDirty&&inWorldOverview())scheduleWorldOverview();
perfEnd('renderBoardNow',renderStarted);
}
function activePath(b){return b.drawing?metaState(b.id).paths[b.drawing.pathIndex]||null:null}
function nextPaint(){return new Promise(resolve=>requestAnimationFrame(()=>resolve()))}
const COMPLETION_NODE_POOL_LIMIT=16,GEM_PARTICLE_POOL_LIMIT=72;
const GEM_PARTICLE_KEYFRAME_TEMPLATE=Object.freeze([
Object.freeze({transform:'translate3d(0,10px,0) scale(.25)',opacity:0}),
Object.freeze({transform:'',opacity:1,offset:.16}),
Object.freeze({transform:'',opacity:1,offset:.56}),
Object.freeze({transform:'',opacity:1,offset:.76}),
Object.freeze({transform:'',opacity:.15})
]),GEM_PARTICLE_ANIMATION_OPTIONS_TEMPLATE=Object.freeze({duration:0,delay:0,easing:'cubic-bezier(.2,.72,.18,1)',fill:'forwards'});
const activeCompletionVisuals=new Map(),completionFlashPool=[],completionBurstPool=[],gemParticlePool=[],activeGemBatches=new Set();
let activeGemParticleCount=0,gemReceivingTimer=0,gemReceivingUntil=0;
function takeCompletionNode(pool,className){
const reused=pool.length>0,node=pool.pop()||document.createElement('div');if(node.isConnected)node.remove();node.className=className;node.removeAttribute('style');node.textContent='';perfCount(reused?'completionNodesReused':'completionNodesCreated');return node;
}
function releaseCompletionNode(node,pool){
if(!node)return;node.remove();node.className='';node.removeAttribute('style');node.textContent='';if(pool.length<COMPLETION_NODE_POOL_LIMIT)pool.push(node);perfGauge('completionPooledNodes',completionFlashPool.length+completionBurstPool.length);
}
function finishCompletionVisual(boardId,skipped=false){
const visual=activeCompletionVisuals.get(boardId);if(!visual)return false;activeCompletionVisuals.delete(boardId);clearTimeout(visual.timer);
visual.board?.timers?.delete?.(visual.timer);visual.board?.card?.classList?.remove('completing');releaseCompletionNode(visual.flash,completionFlashPool);releaseCompletionNode(visual.burst,completionBurstPool);
perfGauge('activeCompletionEffects',activeCompletionVisuals.size);visual.resolve?.({skipped});visual.resolve=null;return true;
}
function completionEffect(b,award){
const started=perfStart();finishCompletionVisual(b.id,true);
let resolve;const finished=new Promise(done=>{resolve=done}),visual={board:b,flash:null,burst:null,timer:0,resolve,finished};
activeCompletionVisuals.set(b.id,visual);perfGauge('activeCompletionEffects',activeCompletionVisuals.size);
try{
if(reducedMotionQuery?.matches){finishCompletionVisual(b.id,false);perfEnd('completionEffectSetup',started);return visual}
b.card.classList.add('completing');
visual.flash=takeCompletionNode(completionFlashPool,'completion-flash');visual.burst=takeCompletionNode(completionBurstPool,'completion-burst');visual.burst.textContent=`◆ +${formatScore(award)}`;b.card.append(visual.flash,visual.burst);
visual.timer=setTimeout(()=>finishCompletionVisual(b.id,false),1800);b.timers?.add?.(visual.timer);
}catch(error){console.warn('BEND FIELD: completion effect skipped',error);finishCompletionVisual(b.id,true)}
perfEnd('completionEffectSetup',started);return visual;
}
function gemCollectionSources(b,count){
const cells=b?.p?.valid||[],rect=b?.svg?.getBoundingClientRect?.(),sources=[];if(!cells.length||!rect?.width)return sources;
const sx=rect.width/b.w,sy=rect.height/b.h,seed=b.meta.seed>>>0;
for(let index=0;index<count;index++){const cell=cells[(Math.imul(index+1,17)+seed)%cells.length],jitterX=.2+((index*37)%61)/100,jitterY=.2+((index*29)%61)/100;sources.push([rect.left+(PAD+(cell[1]+jitterX)*CELL)*sx,rect.top+(PAD+(cell[0]+jitterY)*CELL)*sy])}
return sources;
}
function takeGemParticle(){
const reused=gemParticlePool.length>0,particle=gemParticlePool.pop()||document.createElement('i');particle.remove();particle.className='gem-particle';particle.setAttribute('aria-hidden','true');particle.removeAttribute('style');
if(!particle._gemKeyframes){particle._gemKeyframes=GEM_PARTICLE_KEYFRAME_TEMPLATE.map(frame=>({...frame}));particle._gemAnimationOptions={...GEM_PARTICLE_ANIMATION_OPTIONS_TEMPLATE}}
perfCount(reused?'gemParticlesReused':'gemParticlesCreated');return particle;
}
function releaseGemParticle(batch,particle){
if(!batch?.particles?.has(particle))return false;const animation=batch.particles.get(particle);batch.particles.delete(particle);if(animation){animation.onfinish=null;animation.oncancel=null}
particle.remove();particle.className='';particle.removeAttribute('style');particle._gemDx=particle._gemDy=particle._gemArc=particle._gemDelay=particle._gemDuration=0;if(gemParticlePool.length<GEM_PARTICLE_POOL_LIMIT)gemParticlePool.push(particle);activeGemParticleCount=Math.max(0,activeGemParticleCount-1);perfGauge('activeCosmeticParticles',activeGemParticleCount);perfGauge('gemPooledNodes',gemParticlePool.length);
if(!batch.particles.size){clearTimeout(batch.fallbackTimer);for(const timer of batch.fallbackTimers)clearTimeout(timer);batch.fallbackTimers.clear();activeGemBatches.delete(batch)}return true;
}
function animateGemParticle(particle){
const dx=particle._gemDx,dy=particle._gemDy,arc=particle._gemArc,keyframes=particle._gemKeyframes,options=particle._gemAnimationOptions;
keyframes[1].transform=`translate3d(${arc*.18}px,${-18-Math.abs(arc)*.16}px,0) scale(1.28)`;
keyframes[2].transform=`translate3d(${arc*.28}px,${-25-Math.abs(arc)*.12}px,0) scale(1.05)`;
keyframes[3].transform=`translate3d(${dx*.48+arc}px,${dy*.3-Math.abs(arc)}px,0) scale(.95)`;
keyframes[4].transform=`translate3d(${dx}px,${dy}px,0) scale(.26)`;
options.duration=particle._gemDuration;options.delay=particle._gemDelay;return particle.animate(keyframes,options);
}
function finishGemBatch(batch,cancelAnimations=false){
if(!batch||!activeGemBatches.has(batch))return false;clearTimeout(batch.fallbackTimer);for(const timer of batch.fallbackTimers)clearTimeout(timer);batch.fallbackTimers.clear();
const entries=[...batch.particles];if(cancelAnimations)for(const[,animation]of entries)try{animation?.cancel?.()}catch(_){}
for(const[particle]of entries)releaseGemParticle(batch,particle);activeGemBatches.delete(batch);return true;
}
function scheduleGemReceivingEnd(delay){
const stat=scoreCountEl.closest?.('.stat');stat?.classList.add('gem-receiving');gemReceivingUntil=Math.max(gemReceivingUntil,Date.now()+delay);clearTimeout(gemReceivingTimer);
gemReceivingTimer=setTimeout(()=>{const remaining=gemReceivingUntil-Date.now();if(remaining>8){scheduleGemReceivingEnd(remaining);return}gemReceivingTimer=0;gemReceivingUntil=0;stat?.classList.remove('gem-receiving')},Math.max(0,delay));
}
function cleanupGemEffects(){for(const batch of[...activeGemBatches])finishGemBatch(batch,true);clearTimeout(gemReceivingTimer);gemReceivingTimer=0;gemReceivingUntil=0;scoreCountEl.closest?.('.stat')?.classList.remove('gem-receiving')}
function playGemCollectionAnimation(b,award){
const started=perfStart(),targetRect=scoreCountEl?.getBoundingClientRect?.();if(!b?.card?.isConnected||!targetRect?.width)return false;
const reduced=reducedMotionQuery?.matches,count=reduced?4:Math.max(10,Math.min(18,8+Math.round(Math.log10(Math.max(10,award))*2))),target=[targetRect.left+targetRect.width/2,targetRect.top+targetRect.height/2],
sources=gemCollectionSources(b,count);
if(!sources.length){perfEnd('gemEffectSetup',started);return false}
const batch={particles:new Map(),fallbackTimers:new Set(),fallbackTimer:0},fragment=document.createDocumentFragment();activeGemBatches.add(batch);let latest=0;
sources.forEach(([x,y],index)=>{
const particle=takeGemParticle();particle.style.left=`${x}px`;particle.style.top=`${y}px`;fragment.append(particle);
particle._gemDx=target[0]-x;particle._gemDy=target[1]-y;particle._gemArc=(index%2?-1:1)*(24+(index%5)*9);particle._gemDelay=reduced?0:index*48;particle._gemDuration=reduced?520:1450+(index%4)*110;latest=Math.max(latest,particle._gemDelay+particle._gemDuration);
batch.particles.set(particle,null);activeGemParticleCount++;
});
document.body.append(fragment);perfGauge('activeCosmeticParticles',activeGemParticleCount);perfGauge('gemPooledNodes',gemParticlePool.length);
for(const particle of batch.particles.keys()){
try{
const flight=animateGemParticle(particle);batch.particles.set(particle,flight);flight.onfinish=()=>releaseGemParticle(batch,particle);flight.oncancel=()=>releaseGemParticle(batch,particle);
}catch(_){const delay=particle._gemDelay,duration=particle._gemDuration,timer=setTimeout(()=>{batch.fallbackTimers.delete(timer);releaseGemParticle(batch,particle)},delay+duration);batch.fallbackTimers.add(timer)}
}
batch.fallbackTimer=setTimeout(()=>finishGemBatch(batch,true),latest+250);scheduleGemReceivingEnd(latest+120);perfEnd('gemEffectSetup',started);return true;
}
function skipCompletionVisuals(){for(const boardId of[...activeCompletionVisuals.keys()])finishCompletionVisual(boardId,true)}
document.addEventListener('keydown',event=>{if(event.key==='Escape')skipCompletionVisuals()},true);
function updateSelectedProgress(){}
let selectedProgressFrame=0,pendingProgressBoard=null;
function queueSelectedProgress(b){pendingProgressBoard=b;if(selectedProgressFrame)return;selectedProgressFrame=requestAnimationFrame(()=>{selectedProgressFrame=0;const pending=pendingProgressBoard;pendingProgressBoard=null;if(pending)updateSelectedProgress(pending)})}
const expansionErrorCounts=new Map();
function handleExpansionError(error){
const message=error?.message||String(error),signature=`${error?.name||'Error'}:${message}`,count=(expansionErrorCounts.get(signature)||0)+1;
expansionErrorCounts.set(signature,count);
if(count%20===0)console.warn(`BEND FIELD: expansion is still retrying (${count})`,message);
const workerDelay=error?.code==='WORKER_UNAVAILABLE'?Math.max(1000,workerRetryAt-Date.now()+50):700;scheduleExpansionRepair(workerDelay);
}
function checkSolvedWhenInteractionSettles(b){
if(interactionActive('persistence')){requestAnimationFrame(()=>checkSolvedWhenInteractionSettles(b));return}
checkSolvedAndExpand(b).catch(handleExpansionError);
}
const pendingBoardCommandSettlements=new Map();
let boardCommandSettleFrame=0;
function scheduleBoardCommandSettlement(b,{persist=false,solve=false,paint=true,invalidate=true,pathIndex=null}={}){
if(!b)return;
const pending=pendingBoardCommandSettlements.get(b.id)||{b,persist:false,solve:false,paint:false,invalidate:false,pathIndexes:new Set()};
pending.persist||=persist;pending.solve||=solve;pending.paint||=paint;pending.invalidate||=invalidate;if(Number.isInteger(pathIndex))pending.pathIndexes.add(pathIndex);
pendingBoardCommandSettlements.set(b.id,pending);
if(boardCommandSettleFrame)return;
boardCommandSettleFrame=requestAnimationFrame(()=>{
boardCommandSettleFrame=0;
if(interactionActive('persistence')){scheduleBoardCommandSettlement(b,{paint:false});return}
const settlements=[...pendingBoardCommandSettlements.values()];pendingBoardCommandSettlements.clear();
for(const item of settlements){
const index=item.pathIndexes.size===1?[...item.pathIndexes][0]:null;
if(item.invalidate)invalidateLineGraphCaches(item.b.id,index);
if(item.paint&&item.b.card?.isConnected){renderBoard(item.b);updateSelectedProgress(item.b)}
if(item.persist)changed(item.b.id,index);
if(item.solve)checkSolvedWhenInteractionSettles(item.b);
}
});
}
function applyBoardCommand(b,mutate,{persist=false,solve=false,paint=true,invalidate=true,deferSettlement=false}={}){
const st=metaState(b.id),pathIndex=b.drawing?.pathIndex,pointerInteraction=b.drawing?.pointerId!=null,result=mutate(st);if(result===false)return false;
if(pointerInteraction&&b.drawing?.pointerId==null)refreshInteractionState();
touchBoardClaim(b.id);
if(deferSettlement){scheduleBoardCommandSettlement(b,{persist,solve,paint,invalidate,pathIndex});return result??true}
if(invalidate)invalidateLineGraphCaches(b.id,pathIndex);
if(paint){renderBoard(b);updateSelectedProgress(b)}
if(persist)changed(b.id,pathIndex);
if(solve)checkSolvedWhenInteractionSettles(b);
return result??true;
}
function generationFailureBonusKey(boardId){return`bend-field:generation-failure:${data.worldEpoch||WORLD_GENERATION}:${boardId}`}
async function maybeGrantGenerationFailureBonus(meta,elapsedMs){
if(!meta||elapsedMs<GENERATION_FAILURE_MIN_MS||!unresolvedExpansionCandidates(meta).length)return false;
const marker=generationFailureBonusKey(meta.id);if(safeLocalGet(marker)==='1')return false;
try{
if(onlinePlayerEconomy()){
const result=await fetchJson('/api/player/generation-bonus',{method:'POST',headers:cloudAuthHeaders(),body:JSON.stringify({boardId:meta.id})},10000);applyPlayerEconomyEnvelope(result);serverClockOffset=Number.isFinite(result.serverTime)?result.serverTime-Date.now():serverClockOffset;
}else{
data.bonusEvents=data.bonusEvents||{};data.bonusEvents[`generation-failure:${meta.id}`]=GENERATION_FAILURE_BONUS;data.bonusScore=bonusEventTotal();statsDirty=true;markGlobalDirty();await save(true);
}
safeLocalSet(marker,'1');updateHud();const board=rendered.get(meta.id);if(board?.card?.isConnected)playGemCollectionAnimation(board,GENERATION_FAILURE_BONUS);toast(`生成できなかった盤面の補償 ◆ +${formatScore(GENERATION_FAILURE_BONUS)}`,3200);return true;
}catch(error){if(error?.status!==409)console.warn('BEND FIELD: generation failure bonus skipped',error);return false}
}
async function checkSolvedAndExpand(b){
let st=metaState(b.id);if(st.solved||!isSolved(st,b.p))return false;
if(cloudAvailable&&data.cloudProfile){
await pullCloudWorld(true);st=metaState(b.id);if(st.solved){renderBoard(rendered.get(b.id));updateHud();return false}
if(!isSolved(st,b.p))return false;
}
const previous={state:deepClone(st),lastSolveAt:data.lastSolveAt,timeAttack:deepClone(data.timeAttack),timeAttackRev:data.timeAttackRev,specialMechanicsSeen:[...(data.specialMechanicsSeen||[])],sealedSides:[...(b.meta.sealedSides||[])],metaRev:b.meta.rev};
st.solved=true;hideSpecialTooltip();st.expanded=false;st.solvedBy=currentPlayerName();st.solvedById=currentPlayerId();st.solvedAt=trustedNow();st.scoreVersion=SCORE_VERSION;
const reward=rewardDetailsForBoard(b.meta,st),award=Math.min(MAX_SCORE,reward.award);st.scoreAwarded=award;st.rewardIdentity=reward.identity;st.rewardCoefficient=reward.coefficient;
maybeOpenStore(b.meta,st,award);recordTimeAttackScore(reward);st.rev=nextRevision();markStateDirty(b.id);
const now=trustedNow();data.lastSolveAt=now;data.specialMechanicsSeen=[...new Set([...(data.specialMechanicsSeen||[]),...mechanicTypesForPuzzle(b.p)])].sort();markGlobalDirty();finalizeExitGates(b.meta);
writeDirtyRecoveryJournal();
const persistence=save(true),preparation=prepareExpansionCandidate(b.meta).catch(error=>{if(globalThis.BEND_DEBUG_GENERATION)console.debug('BEND FIELD: speculative expansion preparation skipped',error);return null});
if(!await persistence){
finishCompletionVisual(b.id,true);if(data.metas[b.id]===b.meta&&data.states[b.id]===st){data.states[b.id]=previous.state;data.lastSolveAt=previous.lastSolveAt;data.timeAttack=previous.timeAttack;data.timeAttackRev=previous.timeAttackRev;data.specialMechanicsSeen=previous.specialMechanicsSeen;b.meta.sealedSides=previous.sealedSides;b.meta.rev=previous.metaRev}
renderBoard(rendered.get(b.id));updateHud();throw new Error('保存できなかったため、クリアを取り消しました。');
}
if(cloudAvailable&&data.cloudProfile){
let published=false;
for(let attempt=0;attempt<3&&!published;attempt++){published=await pushCloudPending(b.id);if(!published&&metaState(b.id)?.solved===true)await sleep(180*(attempt+1))}
if(!published){
finishCompletionVisual(b.id,true);
if(data.metas[b.id]===b.meta&&data.states[b.id]===st){data.states[b.id]=previous.state;normalizedStateObjects.add(previous.state);data.lastSolveAt=previous.lastSolveAt;data.timeAttack=previous.timeAttack;data.timeAttackRev=previous.timeAttackRev;data.specialMechanicsSeen=previous.specialMechanicsSeen;b.meta.sealedSides=previous.sealedSides;b.meta.rev=previous.metaRev;markStateDirty(b.id);markGlobalDirty()}
await persistNow({skipCloud:true});restoreCloudPushPending();armCloudPush(500);renderBoard(rendered.get(b.id));updateHud();return false;
}
removeClaim(b.id,'cleared');
}
const immediateBoard=rendered.get(b.id);if(immediateBoard?.card?.isConnected){renderBoard(immediateBoard);playSound('clear');completionEffect(immediateBoard,award);playGemCollectionAnimation(immediateBoard,award)}updateHud();
let durableMeta=data.metas[b.id],durableState=data.states[b.id];if(!durableMeta||!durableState?.solved){scheduleExpansionRepair();return false}scheduleTimeAttackSuggestionAfterCompletion();
const prepared=await preparation;durableMeta=data.metas[b.id];durableState=data.states[b.id];if(!durableMeta||!durableState?.solved){scheduleExpansionRepair();return false}
const expansionStarted=perfNow();let made=0;try{made=await expandMeta(durableMeta,prepared)||0}catch(error){await maybeGrantGenerationFailureBonus(durableMeta,perfNow()-expansionStarted);handleExpansionError(error);return true}
await maybeGrantGenerationFailureBonus(durableMeta,perfNow()-expansionStarted);
if(cloudAvailable&&data.cloudProfile&&made){const shared=await pushCloudPending();if(!shared){await pullCloudWorld(true);scheduleExpansionRepair(500)}}
scheduleExpansionRepair();if(made){expansionErrorCounts.clear();toast(`新しい盤面を${made}枚生成しました。`)}else scheduleExpansionRepair(350);return true;
}
function gateConnectEffect(b,gi,color){
if(!b?.gateLayer||!Number.isInteger(gi))return;const gp=gatePoint(gateObj(b.p,gi));
const pulse=svgEl('circle',{cx:gp[0],cy:gp[1],r:7,class:'gate-connect-pulse',stroke:color||'#fff'});
b.gateLayer.append(pulse);boardTimeout(b,()=>pulse.remove(),720);
b.card.classList.add('gate-connected');boardTimeout(b,()=>b.card.classList.remove('gate-connected'),320);
try{navigator.vibrate?.(18)}catch(_){}
}
function reconcileDrawingPresentation(){refreshInteractionState();scheduleInteractionSettlePresentation()}
function finalizeAtGate(b,gi){
const pointerId=b?.drawing?.pointerId,st=metaState(b.id),pi=b.drawing?.pathIndex,path=pi!=null?st.paths[pi]:null;if(!path||gi==null||!path.detachedStart&&gi===path.startGate||usedGateSet(st,pi).has(gi))return false;
const g=b.p.g[gi],last=path.cells[path.cells.length-1];if(g[0]!==last[0]||g[1]!==last[1])return false;
const color=LINE_COLORS[(path.startColorIndex??path.colorIndex??0)%LINE_COLORS.length],done=applyBoardCommand(b,()=>{
if(path.detachedStart){
path.cells.reverse();path.startGate=gi;path.endGate=null;path.openGate=null;path.detachedStart=false;
const gateColor=canonicalGateColorIndex(b.meta,gi,path);path.colorIndex=gateColor;path.startColorIndex=gateColor;path.endColorIndex=null;
}else{path.endGate=gi;path.openGate=null;const inherited=neighborColor(b.meta,gi);path.endColorIndex=inherited??path.startColorIndex??path.colorIndex}
b.drawing=null;b.armedGate=null;
},{persist:true,solve:true,deferSettlement:true});
if(done){commitConnectedLineVisuals(b,pi);cancelBoardDragFrame(b);clearDragRender(b);hidePickupHandleOverlay();if(pointerId!=null)safeRelease(b.svg,pointerId);playSound('gate');if(usesLightweightDragOverlay(b))requestAnimationFrame(()=>gateConnectEffect(b,gi,color));else gateConnectEffect(b,gi,color);queueMicrotask(reconcileDrawingPresentation)}return done;
}
function openTipMergePlan(b,ai,oi,otherSide='end',enteredOtherTipCell=false){
const st=metaState(b.id);if(ai===oi||!Number.isInteger(ai)||!Number.isInteger(oi))return null;
const a=st.paths[ai],o=st.paths[oi];
if(!a?.cells?.length||!o?.cells?.length||a.endGate!=null||o.endGate!=null||otherSide==='start'&&!o.detachedStart||!a.detachedStart&&!o.detachedStart&&a.startGate===o.startGate)return null;
const orientedOther=otherSide==='start'?[...o.cells].reverse():o.cells,al=a.cells[a.cells.length-1],ol=orientedOther[orientedOther.length-1],tipDistance=manhattan(al,ol);
if(tipDistance!==0&&!(enteredOtherTipCell&&tipDistance===1))return null;
const otherTail=[...orientedOther].reverse().map(cell=>[cell[0],cell[1]]);if(tipDistance===0)otherTail.shift();
const joined=[...a.cells.map(cell=>[cell[0],cell[1]]),...otherTail],seen=new Set();
let cells=joined,merged;
for(const cell of cells){const key=ckey(...cell);if(seen.has(key)||!cellSet(b.p).has(key))return null;seen.add(key)}
const aColor=a.startColorIndex??a.colorIndex,oStartColor=o.startColorIndex??o.colorIndex,oEndColor=o.endColorIndex??oStartColor,oRemainingColor=otherSide==='start'?oEndColor:oStartColor;
const cosmetic={lineEffect:a.lineEffect||o.lineEffect||null,ownerId:a.ownerId||o.ownerId||null};
if(!a.detachedStart&&!o.detachedStart)merged={startGate:a.startGate,endGate:o.startGate,openGate:null,cells,colorIndex:aColor,startColorIndex:aColor,endColorIndex:oStartColor,...cosmetic};
else if(!a.detachedStart&&o.detachedStart)merged={startGate:a.startGate,endGate:null,openGate:null,detachedStart:false,cells,colorIndex:aColor,startColorIndex:aColor,endColorIndex:oRemainingColor,...cosmetic};
else if(a.detachedStart&&!o.detachedStart){cells=[...joined].reverse();merged={startGate:o.startGate,endGate:null,openGate:null,detachedStart:false,cells,colorIndex:oStartColor,startColorIndex:oStartColor,endColorIndex:aColor,...cosmetic}}
else merged={startGate:a.startGate,endGate:null,openGate:null,detachedStart:true,cells,colorIndex:aColor,startColorIndex:aColor,endColorIndex:oRemainingColor,...cosmetic};
return{a,o,lo:Math.min(ai,oi),hi:Math.max(ai,oi),merged};
}
function joinTips(b,ai,oi,otherSide='end',enteredOtherTipCell=false){
if(b?.joiningPaths)return false;const plan=openTipMergePlan(b,ai,oi,otherSide,enteredOtherTipCell);if(!plan)return false;
const st=metaState(b.id),pointerId=b.drawing?.pointerId;b.joiningPaths=true;cancelBoardDragFrame(b);
let done=false;
try{done=applyBoardCommand(b,()=>{
if(st.paths[ai]!==plan.a||st.paths[oi]!==plan.o)return false;
st.paths.splice(plan.hi,1);st.paths.splice(plan.lo,1);st.paths.push(plan.merged);b.drawing=null;
},{persist:true,solve:true,deferSettlement:true})}finally{b.joiningPaths=false}
if(done){commitConnectedLineVisuals(b,metaState(b.id).paths.length-1);if(pointerId!=null)safeRelease(b.svg,pointerId);playSound('gate');queueMicrotask(reconcileDrawingPresentation)}
return done;
}
function pathAxisAtCell(path,p,cell){
const index=path?.cells?.findIndex(candidate=>sameCell(candidate,cell))??-1;if(index<0)return null;
const previous=index?path.cells[index-1]:path.detachedStart?null:outsidePoint(gateObj(p,path.startGate)),next=index<path.cells.length-1?path.cells[index+1]:path.endGate!=null?outsidePoint(gateObj(p,path.endGate)):null;
if(!previous||!next||isWarpTransition(p,previous,cell)||isWarpTransition(p,cell,next))return null;
if(previous[0]===cell[0]&&next[0]===cell[0])return'H';
if(previous[1]===cell[1]&&next[1]===cell[1])return'V';
return null;
}
function activateCrossing(b,cell,owner,last,deferRender=false,segmentOccupancy=null,suppressMotion=false){
const key=ckey(...cell),st=metaState(b.id),pi=b.drawing?.pathIndex,path=pi==null?null:st.paths[pi];if(!path||!b.crossingKeySet.has(key))return false;
const other=st.paths[owner],otherAxis=pathAxisAtCell(other,b.p,cell),approachAxis=last[0]===cell[0]?'H':last[1]===cell[1]?'V':null;
if(!otherAxis||!approachAxis||otherAxis===approachAxis||pathIndexesAtCell(st,key).length!==1)return false;
const touchedKey=specialCellSet(b.p).locks.some(candidate=>sameCell(candidate.key,cell));
applyBoardCommand(b,()=>{path.cells.push(cell);path.openGate=null},{paint:!deferRender,persist:!deferRender,invalidate:!deferRender});
if(touchedKey)playSound('key');
if(segmentOccupancy)segmentOccupancy.set(key,pi);b.drawing?.pathCellIndex?.set(key,path.cells.length-1);playSound('gate');return true;
}
function extendOne(b,cell,deferRender=false,segmentOccupancy=null,suppressMotion=false){
const activeBefore=activePath(b),lastBefore=activeBefore?.cells?.[activeBefore.cells.length-1];
if(b.drawing?.warpDirection&&lastBefore){const dr=cell[0]-lastBefore[0],dc=cell[1]-lastBefore[1];if(dr!==b.drawing.warpDirection[0]||dc!==b.drawing.warpDirection[1])return false;b.drawing.warpDirection=null}
let st=metaState(b.id),path=activePath(b),key=ckey(...cell),occupancyMap=segmentOccupancy||b.drawing?.segmentOccupancy||occupiedMap(st);
if(!path||!cellSet(b.p).has(key))return false;
const pi=b.drawing.pathIndex,last=path.cells[path.cells.length-1];
if(sameCell(cell,last))return true;
if(!pathCellsAdjacent(b.p,cell,last))return false;
const occ=occupancyMap,isCrossing=b.crossingKeySet.has(key),owners=isCrossing?pathIndexesAtCell(st,key):null,currentOwner=isCrossing?(owners.includes(pi)?pi:occ.get(key)):occ.get(key),owner=isCrossing?owners.find(index=>index!==pi):(currentOwner!==pi?currentOwner:null);
if(currentOwner===pi){
let rewind=b.drawing?.pathCellIndex?.get(key)??path.cells.findIndex(candidate=>sameCell(candidate,cell));
if(rewind<0)return false;
const pair=warpPairForCell(b.p,path.cells[rewind]),pairIndex=pair?(b.drawing?.pathCellIndex?.get(ckey(...pair))??path.cells.findIndex(candidate=>sameCell(candidate,pair))):-1;
if(pairIndex===rewind+1)rewind--;
if(rewind<0||path.detachedStart&&rewind===0)return false;
const removed=path.cells.slice(rewind+1),removedWarp=removed.some((candidate,index)=>index&&isWarpTransition(b.p,removed[index-1],candidate));
if(removedWarp&&b.drawing?.pointerId!=null)b.warpedDuringExtend=true;
const result=applyBoardCommand(b,()=>{
path.cells.splice(rewind+1);
if(path.openGate!=null&&!sameCell(path.cells[path.cells.length-1],[b.p.g[path.openGate][0],b.p.g[path.openGate][1]]))path.openGate=null;
},{paint:!deferRender,persist:!deferRender,invalidate:!deferRender});
if(result&&occupancyMap)for(const removedCell of removed){
const removedKey=ckey(...removedCell),remaining=b.crossingKeySet.has(removedKey)?pathIndexesAtCell(st,removedKey).find(index=>index!==pi):null;
if(remaining==null)occupancyMap.delete(removedKey);else occupancyMap.set(removedKey,remaining);
b.drawing?.pathCellIndex?.delete(removedKey);
}
return Boolean(result);
}
const exitDirection=crossingExitDirection(st,b.p,pi);
if(exitDirection){const direction=[cell[0]-last[0],cell[1]-last[1]];if(direction[0]!==exitDirection[0]||direction[1]!==exitDirection[1])return false}
if(owner!=null){
if(activateCrossing(b,cell,owner,last,deferRender,segmentOccupancy,suppressMotion))return true;
const other=st.paths[owner];
if(other?.endGate==null){
const otherSide=other.detachedStart&&sameCell(other.cells[0],cell)?'start':sameCell(other.cells[other.cells.length-1],cell)?'end':null;
if(otherSide)return joinTips(b,pi,owner,otherSide,true);
}
return false;
}
const lock=lockForDoor(b.p,cell);
if(lock&&!pathHasLockKey(path,lock))return false;
const warpExit=warpPairForCell(b.p,cell);
if(warpExit){
const exitKey=ckey(...warpExit),exitOwner=occ.get(exitKey);if(exitOwner!=null&&exitOwner!==pi||path.cells.some(candidate=>sameCell(candidate,warpExit)))return false;
const pointerId=b.drawing?.pointerId;
const beforeWarp=path.cells[path.cells.length-1],warpDirection=[cell[0]-beforeWarp[0],cell[1]-beforeWarp[1]];
applyBoardCommand(b,()=>{path.cells.push(cell,[...warpExit]);path.openGate=null},{paint:!deferRender,persist:!deferRender,invalidate:!deferRender});
if(segmentOccupancy){segmentOccupancy.set(key,pi);segmentOccupancy.set(exitKey,pi)}if(b.drawing?.pathCellIndex){b.drawing.pathCellIndex.set(key,path.cells.length-2);b.drawing.pathCellIndex.set(exitKey,path.cells.length-1)}
if(pointerId!=null)b.warpedDuringExtend=true;if(b.drawing)b.drawing.warpDirection=warpDirection;playSound('warp')
const gi=gateAtCell(b.p,warpExit,path.detachedStart?null:path.startGate);if(gi!=null&&!usedGateSet(st,pi).has(gi))return finalizeAtGate(b,gi);
return true;
}
applyBoardCommand(b,()=>{path.cells.push(cell);path.openGate=null},{paint:!deferRender,persist:!deferRender,invalidate:!deferRender});
if(segmentOccupancy)segmentOccupancy.set(key,pi);b.drawing?.pathCellIndex?.set(key,path.cells.length-1);
if(!suppressMotion){flashConfirmedCell(b,cell);playConfirmTone(path.cells.length)}
const gi=gateAtCell(b.p,cell,path.detachedStart?null:path.startGate);
if(gi!=null&&!usedGateSet(st,pi).has(gi))return finalizeAtGate(b,gi);
return true;
}
function pathTailAxis(path,p){
if(!path?.cells?.length)return null;
const last=path.cells[path.cells.length-1],previous=path.cells.length>1?path.cells[path.cells.length-2]:path.detachedStart?null:outsidePoint(gateObj(p,path.startGate));if(!previous)return null;
if(isWarpTransition(p,previous,last))return null;
return previous[0]===last[0]?'H':previous[1]===last[1]?'V':null;
}
function cellsCrossedBySegment(from,to,{preferredAxis=null,validCells=null}={}){
const x0=(from[0]-PAD)/CELL,y0=(from[1]-PAD)/CELL,x1=(to[0]-PAD)/CELL,y1=(to[1]-PAD)/CELL;
let c=Math.floor(x0),r=Math.floor(y0);
const endC=Math.floor(x1),endR=Math.floor(y1),dc=x1-x0,dr=y1-y0,sx=Math.sign(dc),sy=Math.sign(dr);
const deltaC=sx?1/Math.abs(dc):Infinity,deltaR=sy?1/Math.abs(dr):Infinity;
let maxC=sx>0?(c+1-x0)/Math.abs(dc):sx<0?(x0-c)/Math.abs(dc):Infinity,
maxR=sy>0?(r+1-y0)/Math.abs(dr):sy<0?(y0-r)/Math.abs(dr):Infinity;
const cells=[],limit=600;
for(let guard=0;guard<limit&&(c!==endC||r!==endR);guard++){
const corner=Math.abs(maxC-maxR)<1e-9;
if(corner){
const horizontal=[r,c+sx],vertical=[r+sy,c],horizontalValid=!validCells||validCells.has(ckey(...horizontal)),verticalValid=!validCells||validCells.has(ckey(...vertical));
const chooseHorizontal=horizontalValid&&!verticalValid||horizontalValid===verticalValid&&(preferredAxis==='H'||preferredAxis!=='V'&&Math.abs(dc)>=Math.abs(dr));
if(chooseHorizontal){c+=sx;maxC+=deltaC}else{r+=sy;maxR+=deltaR}
}else if(maxC<maxR){c+=sx;maxC+=deltaC}else{r+=sy;maxR+=deltaR}
cells.push([r,c]);
}
return cells;
}
function gridCellAt(b,pt){
if(!pt)return null;const c=Math.floor((pt[0]-PAD)/CELL),r=Math.floor((pt[1]-PAD)/CELL);
return r>=0&&c>=0&&r<b.p.bounds.h&&c<b.p.bounds.w?[r,c]:null;
}
function rewindActivePathToCell(b,target,segmentOccupancy){
const path=activePath(b);if(!path||!target)return false;
const drawing=b.drawing,key=ckey(...target);let index=drawing?.pathCellIndex?.get(key)??path.cells.findIndex(cell=>sameCell(cell,target));if(index<0||index===path.cells.length-1)return false;
const pair=warpPairForCell(b.p,path.cells[index]),pairIndex=pair?(drawing?.pathCellIndex?.get(ckey(...pair))??path.cells.findIndex(cell=>sameCell(cell,pair))):-1;
if(pairIndex===index+1)index--;if(index<0||path.detachedStart&&index===0)return false;
const removed=path.cells.slice(index+1),removedWarp=removed.some((cell,removedIndex)=>removedIndex&&isWarpTransition(b.p,removed[removedIndex-1],cell));
const result=applyBoardCommand(b,()=>{path.cells.splice(index+1);if(path.openGate!=null&&!sameCell(path.cells[path.cells.length-1],[b.p.g[path.openGate][0],b.p.g[path.openGate][1]]))path.openGate=null},{paint:false,persist:false,invalidate:false});
if(!result)return false;if(removedWarp&&drawing?.pointerId!=null)b.warpedDuringExtend=true;
for(const removedCell of removed){const removedKey=ckey(...removedCell),remaining=b.crossingKeySet.has(removedKey)?pathIndexesAtCell(metaState(b.id),removedKey).find(owner=>owner!==drawing.pathIndex):null;if(remaining==null)segmentOccupancy?.delete(removedKey);else segmentOccupancy?.set(removedKey,remaining);drawing?.pathCellIndex?.delete(removedKey)}
return true;
}
function removeDetachedPathAtOwnEndpoint(b,point,pixels=24){
const drawing=b?.drawing,st=drawing&&metaState(b.id),path=st?.paths?.[drawing.pathIndex];
if(!point||!path?.detachedStart||path.endGate!=null||path.cells.length<2)return false;
const fixed=boardCellCenter(path.cells[0]),radius=Math.min(pointerLocalRadius(b,pixels),CELL*.6);
if((fixed[0]-point[0])**2+(fixed[1]-point[1])**2>radius*radius)return false;
const pointerId=drawing.pointerId,index=drawing.pathIndex;cancelBoardDragFrame(b);
const done=applyBoardCommand(b,()=>{if(st.paths[index]!==path)return false;st.paths.splice(index,1);b.drawing=null;b.armedGate=null},{persist:true,solve:true,deferSettlement:true});
if(done){if(typeof renderBoardNow==='function')renderBoardNow(b);if(pointerId!=null)safeRelease(b.svg,pointerId);playSound('remove');queueMicrotask(reconcileDrawingPresentation)}
return Boolean(done);
}
function extendPointerTo(b,rawPoint,deferFrameRender=false,catchupLimit=DRAG_MAX_LIVE_CATCHUP_CELLS){
const prepareStarted=perfStart();
const drawing=b.drawing,path=activePath(b);if(!drawing||!path||!rawPoint)return;
const pathIndex=drawing.pathIndex;b.specialCrossTriggered=false;b.warpedDuringExtend=false;
const mappedPoint=pointerPointForDrawing(drawing,rawPoint);updateDrawingPointerState(b,drawing,mappedPoint);
const point=mappedPoint,targetCell=gridCellAt(b,point),candidate=drawing.currentCandidateCell,probeStep=6,probeKey=`${drawing.currentConfirmedCell?.join(',')||''}|${targetCell?.join(',')||''}|${candidate?.join(',')||''}|${Math.round(point[0]/probeStep)},${Math.round(point[1]/probeStep)}`;
if(drawing.lastModelProbeKey===probeKey&&!drawing.catchupPending)return;drawing.lastModelProbeKey=probeKey;drawing.catchupPending=false;
const from=boardCellCenter(path.cells[path.cells.length-1]),segmentOccupancy=drawing.segmentOccupancy||(drawing.segmentOccupancy=occupiedMap(metaState(b.id)));let changedPath=false,blocked=false,feedbackCell=null,feedbackLength=0;
perfEnd('pickupModelPrepare',prepareStarted);const topologyStarted=perfStart();
const emitBatchedFeedback=()=>{if(!feedbackCell)return;const now=perfNow();if(now-(b.lastDragToneAt||0)>=90){b.lastDragToneAt=now;playConfirmTone(feedbackLength)}if(uiSettings.lightweightRendering||now-(b.lastDragFeedbackAt||0)<180)return;b.lastDragFeedbackAt=now;flashConfirmedCell(b,feedbackCell)};
updateGateSnapArming(b,drawing);
if(removeDetachedPathAtOwnEndpoint(b,point)){queueSelectedProgress(b);return}
const overlap=overlappingOpenTipAtPoint(b,point);
if(overlap&&joinTips(b,pathIndex,overlap.index,overlap.side)){queueSelectedProgress(b);return}
perfEnd('pickupModelTopology',topologyStarted);const traversalStarted=perfStart();
if(targetCell&&(drawing.pathCellIndex?.has(ckey(...targetCell))||path.cells.some(cell=>sameCell(cell,targetCell))))changedPath=rewindActivePathToCell(b,targetCell,segmentOccupancy)||changedPath;
else{
const crossed=cellsCrossedBySegment(from,point,{preferredAxis:pathTailAxis(activePath(b),b.p),validCells:cellSet(b.p)}),
batch=Number.isFinite(catchupLimit)?crossed.slice(0,Math.max(1,catchupLimit)):crossed;
let catchupHalted=false;
for(const cell of batch){
const key=ckey(...cell),active=activePath(b);if(!active){catchupHalted=true;break}
if(!cellSet(b.p).has(key)){blocked=true;continue}
if(blocked){if(active.cells.some(existing=>sameCell(existing,cell)))changedPath=rewindActivePathToCell(b,cell,segmentOccupancy)||changedPath;continue}
const cellStarted=perfStart(),extended=extendOne(b,cell,true,segmentOccupancy,true);perfEnd('pickupModelCell',cellStarted);
if(!extended){catchupHalted=true;break}
changedPath=true;const updated=activePath(b)||active,tip=updated?.cells?.[updated.cells.length-1];feedbackCell=tip?[...tip]:[...cell];feedbackLength=updated?.cells?.length||1;if(b.specialCrossTriggered||b.warpedDuringExtend)break;
}
if(b.drawing&&crossed.length>batch.length&&!catchupHalted&&!blocked&&!b.specialCrossTriggered&&!b.warpedDuringExtend){b.drawing.catchupPending=true;perfCount('pickupCatchupBatches');perfGauge('pickupCatchupCellsRemaining',crossed.length-batch.length)}
}
perfEnd('pickupModelTraversal',traversalStarted);
if(b.warpedDuringExtend&&b.drawing){
const active=activePath(b),tip=active?.cells?.[active.cells.length-1];
if(tip){const exitPoint=boardCellCenter(tip);b.drawing.pointerOffset=[exitPoint[0]-rawPoint[0],exitPoint[1]-rawPoint[1]];b.drawing.lastPoint=exitPoint;syncDrawingConfirmedState(b,b.drawing)}
if(changedPath){b.dragSpecialRevision++;if(b.drawing)b.drawing.logicalRevision=(b.drawing.logicalRevision||0)+1}
emitBatchedFeedback();
if(!deferFrameRender||b.drawing?.pointerId==null){if(b.drawing?.pointerId!=null)renderDragFrame(b);else renderBoard(b)}if(changedPath)queueSelectedProgress(b);return;
}
if(b.drawing){
updateGateSnapArming(b,drawing);
const active=activePath(b),tip=active?.cells?.[active.cells.length-1]||null;let snappedGate=gateFromPointOrCell(b,mappedPoint);
if(active&&tip){const tipGate=gateFromCell(b,tip,mappedPoint,active.detachedStart?null:active.startGate,20);if(Number.isInteger(tipGate))snappedGate=tipGate}
if(active&&!active.detachedStart&&snappedGate===active.startGate&&active.openGate!==snappedGate)snappedGate=null;
if(active&&snappedGate===drawing.originGate&&!drawing.leftOriginGateCell)snappedGate=null;
if(active&&Number.isInteger(snappedGate)){
const gateCell=[b.p.g[snappedGate][0],b.p.g[snappedGate][1]],currentTip=active.cells[active.cells.length-1];
if(sameCell(gateCell,currentTip)){const snap=boardPointForGate(b,snappedGate).point;b.drawing.lastPoint=snap;finalizeAtGate(b,snappedGate)}
else b.drawing.lastPoint=point;
}else b.drawing.lastPoint=point;
if(b.drawing){syncDrawingConfirmedState(b,b.drawing);updateDrawingPointerState(b,b.drawing,mappedPoint)}
}
if(changedPath){b.dragSpecialRevision++;if(b.drawing)b.drawing.logicalRevision=(b.drawing.logicalRevision||0)+1}
emitBatchedFeedback();
if(!deferFrameRender||b.drawing?.pointerId==null){if(b.drawing?.pointerId!=null)renderDragFrame(b);else renderBoard(b)}if(changedPath)queueSelectedProgress(b)
}
function reopenConnectedFromGate(b,pi,gi,pointerId=null){
const st=metaState(b.id),path=st.paths[pi];
if(!path||path.endGate==null)return false;
if(gi!==path.endGate&&gi!==path.startGate)return false;
const openedColor=canonicalGateColorIndex(b.meta,gi,path);
return applyBoardCommand(b,()=>{
if(gi===path.endGate){
path.endColorIndex=openedColor;path.endGate=null;path.openGate=gi;
}else{
const oldStart=path.startGate,oldEnd=path.endGate,oldEndColor=canonicalGateColorIndex(b.meta,oldEnd,path);
path.cells=[...path.cells].reverse();path.startGate=oldEnd;path.endGate=null;path.openGate=oldStart;
path.startColorIndex=oldEndColor;path.colorIndex=oldEndColor;path.endColorIndex=openedColor;
}
b.armedGate=gi;b.drawing=drawingFromGate(b,pi,gi,pointerId);
},{persist:pointerId==null,paint:pointerId==null});
}
function reopenPathAtCell(b,pi,cell,pointerId=null){
const st=metaState(b.id),path=st.paths[pi];
if(!path)return false;
const idx=path.cells.findIndex(c=>sameCell(c,cell));
if(idx<0)return false;
return applyBoardCommand(b,()=>{
if(path.endGate==null)path.cells.splice(idx+1);
else{const n=path.cells.length,distFromStart=idx,distFromEnd=n-1-idx;if(distFromEnd<=distFromStart){path.cells.splice(idx+1);path.endGate=null;path.endColorIndex=null}else{const oldEnd=path.endGate,oldEndColor=path.endColorIndex,reversed=[...path.cells].reverse(),newIdx=n-1-idx;path.cells=reversed.slice(0,newIdx+1);path.startGate=oldEnd;path.endGate=null;path.startColorIndex=oldEndColor??path.colorIndex;path.colorIndex=path.startColorIndex;path.endColorIndex=null}}
b.drawing=drawingForPath(b,pi,pointerId,null);
},{persist:pointerId==null,paint:pointerId==null});
}
function reverseDetachedPath(path){
if(!path?.detachedStart)return false;
path.cells.reverse();const startColor=path.startColorIndex??path.colorIndex??0,endColor=path.endColorIndex??startColor;
path.startColorIndex=endColor;path.colorIndex=endColor;path.endColorIndex=startColor;path.openGate=null;return true;
}
function detachPathFromStartGate(b,pi,gi,pointerId=null){
const path=metaState(b.id).paths[pi];if(!path||path.detachedStart||path.endGate!=null||path.startGate!==gi||path.cells.length<2)return false;
return applyBoardCommand(b,()=>{
path.cells.reverse();const startColor=path.startColorIndex??path.colorIndex??0,endColor=path.endColorIndex??startColor;
path.startColorIndex=endColor;path.colorIndex=endColor;path.endColorIndex=startColor;path.detachedStart=true;path.openGate=gi;
b.armedGate=gi;b.drawing=drawingFromGate(b,pi,gi,pointerId);
},{persist:pointerId==null,paint:pointerId==null});
}
function orientDetachedEndpoint(b,pi,side,pointerId=null,point=null){
const path=metaState(b.id).paths[pi];if(!path?.detachedStart)return false;
if(side==='start')applyBoardCommand(b,()=>reverseDetachedPath(path),{persist:pointerId==null,paint:pointerId==null});
b.armedGate=null;b.drawing=drawingForPath(b,pi,pointerId,point);return true;
}
function startGate(b,gi,pointerId=null){
const st=metaState(b.id);playSound('grab');
if(st.solved||!Number.isInteger(gi)||!gateConnectionAllowed(b.meta,gi))return;
const existing=st.paths.findIndex(path=>pathUsesGate(path,gi));
if(existing>=0){
const path=st.paths[existing],gateColor=canonicalGateColorIndex(b.meta,gi,path);setPathColorAtGate(path,gi,gateColor);
if(path.endGate!=null){if(reopenConnectedFromGate(b,existing,gi,pointerId)&&pointerId==null)renderBoardNow(b);return}
if(!path.detachedStart&&path.startGate===gi&&path.cells.length>1){if(detachPathFromStartGate(b,existing,gi,pointerId)&&pointerId==null)renderBoardNow(b);return}
if(path.openGate===gi){
path.endColorIndex=canonicalGateColorIndex(b.meta,gi,path);b.armedGate=gi;b.drawing=drawingFromGate(b,existing,gi,pointerId);if(pointerId==null)renderBoardNow(b);
}else if(path.cells.length===1){b.drawing=drawingFromGate(b,existing,gi,pointerId);if(pointerId==null)renderBoardNow(b)}
else toast('\u7dda\u306e\u5148\u7aef\u3092\u3064\u304b\u3093\u3067\u304f\u3060\u3055\u3044\u3002');
return;
}
const g=gateObj(b.p,gi),occ=occupiedMap(st);
if(occ.has(ckey(...g.cell))){renderBoard(b);return}
const colorIndex=canonicalGateColorIndex(b.meta,gi,null);
const started=applyBoardCommand(b,()=>{
st.paths.push({startGate:gi,endGate:null,openGate:null,cells:[[g.cell[0],g.cell[1]]],colorIndex,startColorIndex:colorIndex,endColorIndex:null,lineEffect:activeLineColorItem()?.aurora?'aurora':null,ownerId:currentPlayerId()});
b.drawing=drawingFromGate(b,st.paths.length-1,gi,pointerId);
b.drawing.createdPath=pointerId!=null;
},{persist:pointerId==null,paint:pointerId==null});
if(started&&pointerId==null)renderBoardNow(b);
}
function safeCapture(el,id){try{el.setPointerCapture(id)}catch(_){}finally{refreshInteractionState()}}
function safeRelease(el,id){try{if(el.hasPointerCapture?.(id))el.releasePointerCapture(id)}catch(_){}finally{gestureCoordinator.release(id,'draw');refreshInteractionState();scheduleInteractionSettlePresentation()}}
function eventToSvg(b,e){
const r=boardScreenRect(b),vb=b.svg.viewBox.baseVal;
if(!r.width||!r.height)return null;
return[(e.clientX-r.left)*vb.width/r.width,(e.clientY-r.top)*vb.height/r.height]
}
function cellAt(b,pt){if(!pt)return null;const c=Math.floor((pt[0]-PAD)/CELL),r=Math.floor((pt[1]-PAD)/CELL);return cellSet(b.p).has(ckey(r,c))?[r,c]:null}
function setActiveBoard(id){const previous=rendered.get(activeBoard);let endedDrawing=false;activeBoard=id;if(previous&&previous.id!==id&&previous.drawing?.pointerId==null){endedDrawing=Boolean(previous.drawing);previous.drawing=null;previous.armedGate=null;if(endedDrawing)renderBoard(previous)}if(endedDrawing)queueMicrotask(reconcileDrawingPresentation);return previous}
function selectBoard(b,{paint=true}={}){
if(!b)return;
const alreadySelected=activeBoard===b.id&&data.selectedBoardId===b.id;
setActiveBoard(b.id);data.selectedBoardId=b.id;markGlobalDirty(false);
if(!alreadySelected&&paint)renderBoard(b);updateSelectedProgress(b)
}
function pointerLocalRadius(_board,pixels=50){return pixels/Math.max(MIN_CAMERA_SCALE,cam.scale)}
function pointerFacesGate(b,index,point){
const gate=gateObj(b.p,index),center=boardCellCenter(gate.cell),[dr,dc]=SIDE_D[gate.side];
return(point[0]-center[0])*dc+(point[1]-center[1])*dr>=CELL*.06;
}
function gateCandidatesInCell(b,cell,{exclude=null,interactiveOnly=true}={}){
if(!cell)return[];
const candidates=[],indexes=b.gateIndexesByCell?(b.gateIndexesByCell.get(ckey(...cell))||[]):b.p.g.keys();
for(const index of indexes){
const gate=b.p.g[index];
if(index===exclude||gate[0]!==cell[0]||gate[1]!==cell[1])continue;
if(interactiveOnly&&!gateConnectionAllowed(b.meta,index))continue;
candidates.push(index);
}
return candidates;
}
function gateCandidateAtPoint(b,point,{cellHint=null,directGate=null,exclude=null,interactiveOnly=true,maxPixels=null,requireFacing=false}={}){
if(!point)return Number.isInteger(directGate)?directGate:null;
const cell=cellHint||cellAt(b,point),inCell=gateCandidatesInCell(b,cell,{exclude,interactiveOnly});
if(Number.isInteger(directGate)&&directGate!==exclude&&b.p.g[directGate]&&(!interactiveOnly||gateConnectionAllowed(b.meta,directGate)))return directGate;
let candidates=inCell;
if(!candidates.length){
if(maxPixels===0)return null;
const radius=pointerLocalRadius(b,Number.isFinite(maxPixels)?maxPixels:34),limit=radius*radius;
candidates=[];
for(let index=0;index<b.p.g.length;index++){
if(index===exclude||interactiveOnly&&!gateConnectionAllowed(b.meta,index)||requireFacing&&!pointerFacesGate(b,index,point))continue;
const gp=gatePoint(gateObj(b.p,index)),distance=(gp[0]-point[0])**2+(gp[1]-point[1])**2;
if(distance<=limit)candidates.push(index);
}
}
if(!candidates.length)return null;
if(candidates.length===1)return candidates[0];
let best=candidates[0],bestDistance=Infinity;
for(const index of candidates){
const gp=gatePoint(gateObj(b.p,index)),distance=(gp[0]-point[0])**2+(gp[1]-point[1])**2;
if(distance<bestDistance){best=index;bestDistance=distance}
}
return best;
}
function rebuildEndpointIndexes(b,st=metaState(b.id)){
const indexByCell=new Map(),add=(cell,index,side)=>{if(!cell)return;const key=ckey(...cell),entries=indexByCell.get(key)||[];entries.push({index,side,cell});indexByCell.set(key,entries)};
st.paths.forEach((path,index)=>{if(path?.endGate!=null||!path?.cells?.length)return;add(path.cells[path.cells.length-1],index,'end');if(path.detachedStart&&path.cells.length>1)add(path.cells[0],index,'start')});
b.endpointIndexesByCell=indexByCell;return indexByCell;
}
function endpointCandidatesNearPoint(b,point){
const cell=gridCellAt(b,point);if(!cell)return[];
const index=b.endpointIndexesByCell||rebuildEndpointIndexes(b),candidates=[];
for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++)for(const entry of index.get(ckey(cell[0]+dr,cell[1]+dc))||[])candidates.push(entry);
return candidates;
}
function nearestEndpointAtPoint(b,point){
if(!point)return null;const radius=Math.min(pointerLocalRadius(b,30),CELL*.48),limit=radius*radius;
let best=null,bestD=limit;
for(const entry of endpointCandidatesNearPoint(b,point)){const tip=boardCellCenter(entry.cell),d=(tip[0]-point[0])**2+(tip[1]-point[1])**2;if(d<bestD){best=entry.index;bestD=d}}
return best;
}
function overlappingOpenTipAtPoint(b,point,pixels=42){
const drawing=b?.drawing,st=drawing&&metaState(b.id),active=st?.paths?.[drawing.pathIndex];if(!point||!active?.cells?.length)return null;
const activeTip=active.cells[active.cells.length-1],radius=Math.min(pointerLocalRadius(b,pixels),CELL*.82),limit=radius*radius;let best=null,bestDistance=limit;
const endpointIndex=b.endpointIndexesByCell||rebuildEndpointIndexes(b,st);
for(const entry of endpointIndex.get(ckey(...activeTip))||[]){if(entry.index===drawing.pathIndex)continue;const center=boardCellCenter(entry.cell),distance=(center[0]-point[0])**2+(center[1]-point[1])**2;if(distance<=bestDistance){best={index:entry.index,side:entry.side};bestDistance=distance}}
return best;
}
function boardPointForGate(b,gateIndex){const gp=gatePoint(gateObj(b.p,gateIndex));return {index:gateIndex,point:gp,cell:[b.p.g[gateIndex][0],b.p.g[gateIndex][1]]}}
function drawingFromGate(b,pathIndex,gateIndex,pointerId=null){
const gateCell=[b.p.g[gateIndex][0],b.p.g[gateIndex][1]],point=boardPointForGate(b,gateIndex).point;
const drawing=drawingForPath(b,pathIndex,pointerId,point);
drawing.originGate=gateIndex;drawing.originGateCell=gateCell;return drawing;
}
function updateGateSnapArming(b,drawing){
if(!drawing||drawing.gateSnapArmed||!Number.isInteger(drawing.originGate)||!Array.isArray(drawing.originGateCell))return;
const path=metaState(b.id).paths[drawing.pathIndex],currentCell=path?.cells?.[path.cells.length-1];
if(currentCell&&!sameCell(currentCell,drawing.originGateCell)){drawing.gateSnapArmed=true;drawing.leftOriginGateCell=true}
}
function canonicalGateColorIndex(meta,gateIndex,path=null){
const inherited=neighborColor(meta,gateIndex);
return inherited??pathColorIndexAtGate(path,gateIndex)??chooseColor(meta,gateIndex)??0;
}
function gateFromCell(b,cell,point=null,exclude=null,maxPixels=null){
if(!cell)return null;
const reference=point||boardCellCenter(cell);
return gateCandidateAtPoint(b,reference,{cellHint:cell,exclude,maxPixels});
}
function gateFromPointOrCell(b,point,cellHint=null){
const exactCell=cellHint||cellAt(b,point);if(!exactCell)return null;
return gateCandidateAtPoint(b,point,{cellHint:exactCell,maxPixels:0});
}
function gateStartCandidate(b,point,cellHint=null,directGate=null){return gateCandidateAtPoint(b,point,{cellHint,directGate,maxPixels:34})}
function armGate(b,gateIndex){
b.armedGate=Number.isInteger(gateIndex)?gateIndex:null;
if(Number.isInteger(gateIndex))b.gateKnobs[gateIndex]?.classList.add('show');
}
function edgePanVelocity(clientX,clientY){
const rect=getViewportRect(),margin=Math.min(DRAG_EDGE_MARGIN,rect.width*.22,rect.height*.22),axis=(value,min,max)=>value<min+margin?-(1-(value-min)/margin):value>max-margin?1-(max-value)/margin:0;
return[axis(clientX,rect.left,rect.right)*DRAG_EDGE_MAX_SPEED,axis(clientY,rect.top,rect.bottom)*DRAG_EDGE_MAX_SPEED];
}
function pointerInsideBoardScreen(b,move){const rect=boardScreenRect(b);return Boolean(move&&rect.width&&rect.height&&move.clientX>=rect.left&&move.clientX<=rect.right&&move.clientY>=rect.top&&move.clientY<=rect.bottom)}
function hidePickupHandleOverlay(){if(!pickupHandleOverlay)return;pickupHandleOverlay.classList.remove('visible');pickupHandleOverlay.style.removeProperty('transform')}
function syncPickupHandleDesign(){
if(!pickupHandleOverlay)return false;
const item=activeCustomCursorItem(),style=item?.cursorStyle||'default';if(pickupHandleOverlay.dataset.cursorStyle===style)return Boolean(item);
pickupHandleOverlay.dataset.cursorStyle=style;pickupHandleOverlay.classList.toggle('custom-cursor',Boolean(item));pickupHandleOverlay.classList.toggle('flag-cursor',Boolean(item?.flagAsset));
if(item?.flagAsset){const image=document.createElement('img');image.src=item.flagAsset;image.alt='';image.draggable=false;pickupHandleOverlay.replaceChildren(image)}else pickupHandleOverlay.textContent=item?.cursorEmoji||'';
return Boolean(item);
}
function activeDrawingLineColorIndex(path){return Number.isInteger(path?.startColorIndex)?path.startColorIndex:Number.isInteger(path?.colorIndex)?path.colorIndex:0}
function updatePickupHandleOverlay(b,drawing,clientX,clientY){
if(!pickupHandleOverlay)return false;if(!drawing||!Number.isFinite(clientX)||!Number.isFinite(clientY)){hidePickupHandleOverlay();return false}
const path=activePath(b),color=LINE_COLORS[activeDrawingLineColorIndex(path)%LINE_COLORS.length],transform=`translate3d(${clientX}px,${clientY}px,0) translate(-50%,-50%)`;
syncPickupHandleDesign();if(pickupHandleOverlay.style.transform!==transform)pickupHandleOverlay.style.transform=transform;
if(pickupHandleOverlay.style.getPropertyValue('--pickup-color')!==color)pickupHandleOverlay.style.setProperty('--pickup-color',color);
pickupHandleOverlay.classList.add('visible');return true;
}
function usesLightweightDragOverlay(b){return uiSettings.lightweightRendering&&(b?.p?.valid?.length||0)>=LIGHTWEIGHT_DRAG_BOARD_CELLS}
function setLightweightDragPresentation(b,active){
if(!b||!usesLightweightDragOverlay(b))return false;const next=Boolean(active);if(b.lightweightDragPresentation===next)return false;b.lightweightDragPresentation=next;
for(const layer of[b.connectorLayer,b.pathLayer,b.specialLayer,b.gateLayer,b.numberLayer])if(layer)layer.style.opacity=next?'0':'';
return true;
}
function setPickupScenePresentation(activeBoard=null,active=false){
const next=Boolean(active);
for(const board of rendered.values())setLightweightDragPresentation(board,next&&board===activeBoard);
}
function recordPickupStartFrame(b,drawing){
if(!drawing||drawing.pickupStartMeasured)return false;drawing.pickupStartMeasured=true;
perfGauge('pickupStartFullBoardRenders',Math.max(0,(perfCounters.fullBoardRenders||0)-(drawing.fullBoardRendersAtPointerDown||0)));
if(Number.isFinite(drawing.pointerDownAt))perfObserve('pickupStartLatency',Math.max(0,perfNow()-drawing.pointerDownAt));return true;
}
function ensureBoardDragScheduler(b){
if(!b)return null;
if(!b.dragScheduler)b.dragScheduler=DragSchedulerApi.createDragScheduler({
maxSamples:DRAG_MAX_POINTER_SAMPLES,
trim:trimBoardPointerSamples,
interval:DRAG_FRAME_INTERVAL,
tolerance:INTERACTION_FRAME_TOLERANCE_MS,
watchdogDelay:DRAG_DISPLAY_WATCHDOG_MS,
now:perfNow,
onWatchdog:()=>perfCount('pickupDisplayWatchdogFrames'),
onFrame:(_scheduler,timestamp)=>{
if(b.releaseDrain)processBoardPointerReleaseDrain(b,b.releaseDrain,timestamp);
else processBoardDragFrame(b,timestamp);
}
});
return b.dragScheduler;
}
function cancelBoardDragFrame(b){
if(!b)return;
b.dragScheduler?.cancel();b.releaseDrain=null;
b.lastProcessedPointerMove=null;b.dragFrameAt=0;b.dragNextFrameAt=0;b.dragVisualActiveUntil=0;b.dragVisualX=NaN;b.dragVisualY=NaN;b.dragVisualLastFrameAt=0;b.dragVisualCommitAt=0;b.dragSvgVisualAt=0;b.dragTrailRevision=-1;hidePickupHandleOverlay();
}
function scheduleBoardDragFrame(b){
return Boolean(ensureBoardDragScheduler(b)?.requestFrame());
}
function pointerEventSamples(event){
let sample=event;if(event?.pointerType!=='mouse'&&typeof event?.getCoalescedEvents==='function'){const coalesced=event.getCoalescedEvents();if(coalesced?.length)sample=coalesced[coalesced.length-1]}
return sample&&Number.isFinite(sample.clientX)&&Number.isFinite(sample.clientY)?{pointerId:event.pointerId,clientX:sample.clientX,clientY:sample.clientY,inputAt:Number(sample.timeStamp)||Number(event.timeStamp)||perfNow()}:null;
}
function trimBoardPointerSamples(samples){
while(samples.length>DRAG_MAX_POINTER_SAMPLES){
let removeIndex=1,leastTurn=Infinity;
for(let index=1;index<samples.length-1;index++){
const a=samples[index-1],b=samples[index],c=samples[index+1],
turn=Math.abs((b.clientX-a.clientX)*(c.clientY-b.clientY)-(b.clientY-a.clientY)*(c.clientX-b.clientX));
if(turn<leastTurn){leastTurn=turn;removeIndex=index}
}
samples.splice(removeIndex,1);
}
return samples;
}
function setBoardPointerSample(b,sample){
if(!b||!sample)return null;
const scheduler=ensureBoardDragScheduler(b);
if(!scheduler.push(sample))return null;
b.dragVisualActiveUntil=perfNow()+40;return sample;
}
function appendBoardPointerSamples(b,event){return setBoardPointerSample(b,pointerEventSamples(event))}
function processBoardDragFrame(b,timestamp){
const started=perfStart(),scheduler=ensureBoardDragScheduler(b),dragState=scheduler.inspect();perfCount('dragFrames');markVisualFrame(timestamp);const move=scheduler.latest(),drawing=b.drawing;if(!move||!drawing||drawing.pointerId!==move.pointerId){cancelBoardDragFrame(b);perfEnd('processBoardDragFrame',started);return}
const logicalSlotDue=!b.dragNextFrameAt||timestamp+2>=b.dragNextFrameAt,
freshVisualInput=dragState.freshVisual,
freshLogicalInput=dragState.freshLogical;
if(freshVisualInput){recordInteractionCommit('pickupVisual',timestamp,move.inputAt,true);b.dragVisualCommitAt=timestamp;scheduler.markVisual()}
else if(timestamp<=b.dragVisualActiveUntil)recordInteractionCommit('pickupVisual',timestamp,move.inputAt,false);
b.lastProcessedPointerMove=move;
if(logicalSlotDue){let nextFrame=b.dragNextFrameAt||timestamp;do nextFrame+=DRAG_FRAME_INTERVAL;while(nextFrame<=timestamp+2);b.dragNextFrameAt=nextFrame}
b.dragVisualX=move.clientX;b.dragVisualY=move.clientY;b.dragVisualLastFrameAt=timestamp;
touchBoardClaim(b.id);const velocity=edgePanVelocity(move.clientX,move.clientY),dt=Math.min(32,Math.max(8,timestamp-(b.dragFrameAt||timestamp-16)));b.dragFrameAt=timestamp;
if(velocity[0]||velocity[1]){cam.x-=velocity[0]*dt;cam.y-=velocity[1]*dt;applyCamera(true)}
const logicalDue=logicalSlotDue&&(freshLogicalInput||velocity[0]||velocity[1]||b.drawing?.catchupPending||scheduler.hasLogical());
if(logicalDue){
const modelStarted=perfStart();let processed=0,logicalMove=move;
do{
logicalMove=freshLogicalInput&&scheduler.hasLogical()?scheduler.takeLogical(1)[0]:move;processed++;
const point=pointerInsideBoardScreen(b,logicalMove)?eventToSvg(b,logicalMove):null,traceCell=point?cellAt(b,point):null;
if(traceCell){const trace=b.logicalPointerCellTrace||(b.logicalPointerCellTrace=[]);trace.push(traceCell);if(trace.length>48)trace.shift()}
if(point&&b.drawing?.pointerId===logicalMove.pointerId)extendPointerTo(b,point,true);
}while(freshLogicalInput&&scheduler.hasLogical()&&processed<DRAG_MAX_LOGICAL_SAMPLES_PER_FRAME&&perfNow()-modelStarted<DRAG_MODEL_BUDGET_MS)
recordInteractionCommit('pickupLogical',timestamp,logicalMove.inputAt,freshLogicalInput);
if(freshLogicalInput&&!scheduler.hasLogical())scheduler.markLogical();
perfEnd('pickupModelWork',modelStarted);
}
const visualStarted=perfStart();
const visualMove=b.dragVisualMove||(b.dragVisualMove={clientX:0,clientY:0});visualMove.clientX=b.dragVisualX;visualMove.clientY=b.dragVisualY;
updatePickupHandleOverlay(b,b.drawing,b.dragVisualX,b.dragVisualY);
const visualPoint=eventToSvg(b,visualMove);if(visualPoint&&b.drawing?.pointerId===move.pointerId)updateDrawingHandlePosition(b,b.drawing,visualPoint);
if(usesLightweightDragOverlay(b)){recordPickupStartFrame(b,b.drawing);dirtyBoards.delete(b)}
else if(b.drawing?.pointerId===move.pointerId)renderDragFrame(b);
perfEnd('pickupVisualWork',visualStarted);
if((perfNow()<b.dragVisualActiveUntil||velocity[0]||velocity[1]||b.drawing?.catchupPending||scheduler.hasLogical())&&b.drawing?.pointerId===move.pointerId)scheduleBoardDragFrame(b);
const workDuration=perfEnd('processBoardDragFrame',started);observeInteractionFrame(timestamp,workDuration);
}
function queueBoardPointerMove(b,event){
appendBoardPointerSamples(b,event);
scheduleBoardDragFrame(b);
}
function scheduleBoardPointerReleaseDrain(b){
const drain=b?.releaseDrain;if(!drain)return false;
ensureBoardDragScheduler(b).requestFrame();return true;
}
function processBoardPointerReleaseDrain(b,drain,timestamp){
if(b.releaseDrain!==drain)return;
const started=perfStart();let processed=0;
while(drain.index<drain.moves.length&&processed<DRAG_MAX_LOGICAL_SAMPLES_PER_FRAME&&perfNow()-started<DRAG_MODEL_BUDGET_MS){
const move=drain.moves[drain.index],point=pointerInsideBoardScreen(b,move)?eventToSvg(b,move):null;
if(!drain.tracedIndexes.has(drain.index)){drain.tracedIndexes.add(drain.index);const traceCell=point?cellAt(b,point):null;if(traceCell){const trace=b.logicalPointerCellTrace||(b.logicalPointerCellTrace=[]);b.lastFlushPointerCells.push(traceCell);trace.push(traceCell);if(trace.length>48)trace.shift()}}
if(point&&b.drawing?.pointerId===move.pointerId)extendPointerTo(b,point,true,DRAG_MAX_RELEASE_CATCHUP_CELLS);
processed++;
if(b.releaseDrain!==drain){queueMicrotask(drain.onComplete);perfEnd('pickupReleaseDrainWork',started);return}
if(!b.drawing||b.drawing.pointerId!==drain.pointerId||!b.drawing.catchupPending)drain.index++;
}
if(b.drawing&&!usesLightweightDragOverlay(b))renderDragFrame(b);
recordInteractionCommit('pickupReleaseDrain',timestamp,drain.inputAt,true);perfCount('pickupReleaseDrainFrames');perfEnd('pickupReleaseDrainWork',started);
if(drain.index<drain.moves.length&&b.drawing?.pointerId===drain.pointerId){scheduleBoardPointerReleaseDrain(b);return}
b.releaseDrain=null;ensureBoardDragScheduler(b).beginSettling();queueMicrotask(drain.onComplete);
}
function flushBoardPointerMove(b,event,onComplete=()=>{}){
const scheduler=ensureBoardDragScheduler(b),sample=event?pointerEventSamples(event):null;
scheduler.beginDrain(sample);const moves=scheduler.drainLogical();
b.lastProcessedPointerMove=null;b.dragFrameAt=0;b.dragNextFrameAt=0;b.dragVisualActiveUntil=0;b.dragVisualX=NaN;b.dragVisualY=NaN;b.dragVisualLastFrameAt=0;b.dragVisualCommitAt=0;b.dragSvgVisualAt=0;b.dragTrailRevision=-1;hidePickupHandleOverlay();
b.lastFlushPointerCells=[];
if(!moves.length||!b.drawing){queueMicrotask(onComplete);return false}
b.releaseDrain={pointerId:b.drawing.pointerId,moves,index:0,inputAt:moves[moves.length-1]?.inputAt||perfNow(),lastFrameAt:0,tracedIndexes:new Set(),onComplete};
scheduleBoardPointerReleaseDrain(b);return true;
}
function reconnectOpenGateOnRelease(b){
const drawing=b?.drawing,path=activePath(b),gateIndex=path?.openGate;
if(!drawing||!path||!Number.isInteger(gateIndex)||drawing.leftOriginGateCell)return false;
const gateCell=[b.p.g[gateIndex][0],b.p.g[gateIndex][1]],tip=path.cells[path.cells.length-1];
if(!sameCell(gateCell,tip))return false;
return finalizeAtGate(b,gateIndex);
}
function discardUnmovedCreatedPath(b,drawing=b?.drawing){
if(!drawing?.createdPath)return false;
const st=metaState(b.id),path=st.paths[drawing.pathIndex];if(!path||path.endGate!=null||path.cells.length!==1)return false;
return applyBoardCommand(b,()=>{st.paths.splice(drawing.pathIndex,1);b.drawing=null;b.armedGate=null},{persist:true,deferSettlement:true});
}
function schedulePendingClaimPreview(b){
if(!b?.pendingClaimPointer||b.pendingClaimFrame)return;
const step=timestamp=>{
const pending=b.pendingClaimPointer;if(!pending){b.pendingClaimFrame=0;return}
if(pending.lastFrameAt&&timestamp+INTERACTION_FRAME_TOLERANCE_MS<pending.lastFrameAt+DRAG_FRAME_INTERVAL){b.pendingClaimFrame=requestAnimationFrame(step);return}
b.pendingClaimFrame=0;const point=eventToSvg(b,pending);if(!point)return;
pending.lastFrameAt=timestamp;pending.preview.setAttribute('transform',`translate(${point[0]} ${point[1]})`);recordInteractionCommit('pickupPreview',timestamp,pending.inputAt);markVisualFrame(timestamp);perfCount('pickupPreviewFrames');
};
b.pendingClaimFrame=requestAnimationFrame(step);
}
function beginPendingClaimPointer(b,event,targetInfo){
const started=perfStart();
if(b.pendingClaimPointer)clearPendingClaimPointer(b,b.pendingClaimPointer.pointerId);
const preview=svgEl('g',{class:'claim-pending-preview'}),halo=svgEl('circle',{r:9,class:'claim-pending-halo'}),knob=svgEl('circle',{r:5.5,class:'claim-pending-knob'});
preview.append(halo,knob);b.claimPreviewLayer.replaceChildren(preview);
const initialSample=pointerEventSamples(event);
b.pendingClaimPointer={pointerId:event.pointerId,clientX:event.clientX,clientY:event.clientY,inputAt:Number(event.timeStamp)||perfNow(),samples:initialSample?[initialSample]:[],startedAt:perfNow(),fullBoardRendersAtDown:perfCounters.fullBoardRenders||0,preview,...targetInfo};
try{b.svg.setPointerCapture(event.pointerId)}catch(_){}refreshInteractionState();schedulePendingClaimPreview(b);perfEnd('pickupPointerDownPreview',started);return b.pendingClaimPointer;
}
function updatePendingClaimPointer(b,event){
const pending=b?.pendingClaimPointer;if(!pending||pending.pointerId!==event.pointerId||pending.released)return false;
const sample=pointerEventSamples(event);if(sample){pending.clientX=sample.clientX;pending.clientY=sample.clientY;pending.inputAt=sample.inputAt;(pending.samples||(pending.samples=[])).push(sample);trimBoardPointerSamples(pending.samples);schedulePendingClaimPreview(b)}return true;
}
function markPendingClaimPointerReleased(b,event){
const pending=b?.pendingClaimPointer;if(!pending||pending.pointerId!==event.pointerId)return false;
const sample=pointerEventSamples(event)||{pointerId:event.pointerId,clientX:pending.clientX,clientY:pending.clientY,inputAt:Number(event.timeStamp)||perfNow()};
pending.clientX=sample.clientX;pending.clientY=sample.clientY;pending.inputAt=sample.inputAt;pending.released=true;pending.releaseSample=sample;(pending.samples||(pending.samples=[])).push(sample);trimBoardPointerSamples(pending.samples);
if(b.pendingClaimFrame)cancelAnimationFrame(b.pendingClaimFrame);b.pendingClaimFrame=0;event.preventDefault?.();return true;
}
function clearPendingClaimPointer(b,pointerId,{release=true,releaseGesture=release}={}){
const pending=b?.pendingClaimPointer;if(!pending||pointerId!=null&&pending.pointerId!==pointerId)return null;
if(b.pendingClaimFrame)cancelAnimationFrame(b.pendingClaimFrame);b.pendingClaimFrame=0;b.pendingClaimPointer=null;b.claimPreviewLayer.replaceChildren();
if(release)try{if(b.svg.hasPointerCapture?.(pending.pointerId))b.svg.releasePointerCapture(pending.pointerId)}catch(_){}
if(releaseGesture)gestureCoordinator.release(pending.pointerId,'draw');
refreshInteractionState();return pending;
}
function recoverLostBoardPointerCapture(b,event){
const pointerId=event?.pointerId,drawing=b?.drawing;if(pointerId==null||drawing?.pointerId!==pointerId||!realtimeHeldPointers.has(pointerId))return false;
requestAnimationFrame(()=>{if(b.card?.isConnected&&b.drawing?.pointerId===pointerId&&realtimeHeldPointers.has(pointerId))safeCapture(b.svg,pointerId)});return true;
}
function activateBoardPointerDrag(b,pending,point){
const started=perfStart();
const drawing=b?.drawing;if(!drawing||!pending||!point){perfEnd('pickupPointerDownCommit',started);return false}
cancelBoardDragFrame(b);
b.logicalPointerCellTrace=[];b.lastFlushPointerCells=[];
drawing.lastPoint=point;updateDrawingPointerState(b,drawing,point);drawing.pointerDownAt=pending.startedAt;drawing.fullBoardRendersAtPointerDown=pending.fullBoardRendersAtDown;
activateBoardHud(b);setPickupScenePresentation(b,true);refreshInteractionState();
safeCapture(b.svg,pending.pointerId);
const samples=pending.samples?.length?pending.samples:[{pointerId:pending.pointerId,clientX:pending.clientX,clientY:pending.clientY,inputAt:pending.inputAt}];
for(const sample of samples)setBoardPointerSample(b,sample);
const latest=samples[samples.length-1];if(latest){b.dragVisualX=latest.clientX;b.dragVisualY=latest.clientY;updatePickupHandleOverlay(b,drawing,latest.clientX,latest.clientY)}
scheduleBoardDragFrame(b);perfEnd('pickupPointerDownCommit',started);return true;
}
function bindBoard(b){
b.card.addEventListener('focus',()=>selectBoard(b));
b.svg.addEventListener('pointerdown',async e=>{
const endpointTarget=e.target.closest?.('.endpoint-hit'),gateTarget=e.target.closest?.('.gate-hit'),boardTarget=e.target.closest?.('.board-input-surface');
if(e.button!==0||b.joiningPaths||!endpointTarget&&!gateTarget&&!boardTarget||!gestureCoordinator.claim(e.pointerId,'draw'))return;
e.preventDefault();
e.stopPropagation();
selectBoard(b,{paint:false});
let st=metaState(b.id);
if(st.solved){gestureCoordinator.release(e.pointerId,'draw');return}
const directGate=Number(gateTarget?.dataset.gate),targetEndpoint=Number(endpointTarget?.dataset.pathIndex),targetEndpointSide=endpointTarget?.dataset.endpointSide||'end',
pending=beginPendingClaimPointer(b,e,{directGate,targetEndpoint,targetEndpointSide});
const claimApproved=await ensureBoardClaimForInput(b);
if(!claimApproved||b.pendingClaimPointer!==pending||!pending.released&&!realtimeHeldPointers.has(e.pointerId)){clearPendingClaimPointer(b,e.pointerId);return}
clearPendingClaimPointer(b,e.pointerId,{release:false});
if(!b.card.isConnected||(st=metaState(b.id)).solved){safeRelease(b.svg,e.pointerId);return}touchBoardClaim(b.id,true);
const point=eventToSvg(b,pending),finishReleased=activated=>{if(!pending.released)return activated;if(activated)queueMicrotask(()=>finishPointer(pending.releaseSample||pending,true));else safeRelease(b.svg,e.pointerId);return activated};
if(Number.isInteger(directGate)&&b.p.g[directGate]){
const directCell=[b.p.g[directGate][0],b.p.g[directGate][1]],occupiedPath=occupiedMap(st).get(ckey(...directCell)),gatePath=st.paths.findIndex(path=>pathUsesGate(path,directGate));
if(gatePath>=0||occupiedPath==null){armGate(b,directGate);startGate(b,directGate,e.pointerId);const activated=activateBoardPointerDrag(b,pending,point);if(!activated){b.armedGate=null;renderBoard(b)}finishReleased(activated);return}
}
if(Number.isInteger(targetEndpoint)&&st.paths[targetEndpoint]?.endGate==null){
if(st.paths[targetEndpoint].detachedStart)orientDetachedEndpoint(b,targetEndpoint,targetEndpointSide,e.pointerId,point);
else{b.armedGate=null;b.drawing=drawingForPath(b,targetEndpoint,e.pointerId,point)}
finishReleased(activateBoardPointerDrag(b,pending,point));return;
}
const nearestEndpoint=nearestEndpointAtPoint(b,point);
if(Number.isInteger(nearestEndpoint)&&st.paths[nearestEndpoint]?.endGate==null){
b.armedGate=null;b.drawing=drawingForPath(b,nearestEndpoint,e.pointerId,point);
finishReleased(activateBoardPointerDrag(b,pending,point));return;
}
const hintCell=cellAt(b,point),
targetGate=gateStartCandidate(b,point,hintCell,directGate),occupiedPath=hintCell?occupiedMap(st).get(ckey(...hintCell)):null,
gatePath=Number.isInteger(targetGate)?st.paths.findIndex(path=>pathUsesGate(path,targetGate)):-1;
if(Number.isInteger(targetGate)&&(gatePath>=0||occupiedPath==null)){
armGate(b,targetGate);startGate(b,targetGate,e.pointerId);
const activated=activateBoardPointerDrag(b,pending,point);if(!activated){b.armedGate=null;renderBoard(b)}finishReleased(activated);
return;
}
if(Number.isInteger(occupiedPath)){
if(reopenPathAtCell(b,occupiedPath,hintCell,e.pointerId)){playSound('grab');finishReleased(activateBoardPointerDrag(b,pending,point));return}
}
if(Number.isInteger(targetGate)){
armGate(b,targetGate);startGate(b,targetGate,e.pointerId);
const activated=activateBoardPointerDrag(b,pending,point);if(!activated){b.armedGate=null;renderBoard(b)}finishReleased(activated);
return;
}
const cell=cellAt(b,point);
if(!cell){finishReleased(false);return;}
const pi=occupiedMap(st).get(ckey(...cell));
if(pi!=null){
if(reopenPathAtCell(b,pi,cell,e.pointerId)){
finishReleased(activateBoardPointerDrag(b,pending,point));return;
}
}
finishReleased(false);
});
b.svg.addEventListener('pointermove',e=>{
if(updatePendingClaimPointer(b,e)){e.preventDefault();return}
if(!b.drawing||b.drawing.pointerId!==e.pointerId)return;
e.preventDefault();queueBoardPointerMove(b,e);
});
const finishPointer=(e,flush=true)=>{
const started=perfStart();try{
if(b.pendingClaimPointer?.pointerId===e.pointerId){if(flush===true)markPendingClaimPointerReleased(b,e);else clearPendingClaimPointer(b,e.pointerId);return}
if(b.releaseDrain?.pointerId===e.pointerId)return;
if(!b.drawing){cancelBoardDragFrame(b);clearDragRender(b);hidePickupHandleOverlay();safeRelease(b.svg,e.pointerId);return}
if(b.drawing.pointerId!==e.pointerId)return;
const release=(paintCurrent=false)=>{if(paintCurrent&&b.card?.isConnected)renderBoardNow(b);cancelBoardDragFrame(b);clearDragRender(b);hidePickupHandleOverlay();safeRelease(b.svg,e.pointerId);queueMicrotask(()=>updateCustomCursorFromPointer(e))};
if(flush===true){flushBoardPointerMove(b,e,()=>finishPointer(e,'settled'));return}
if(flush!=='settled')cancelBoardDragFrame(b);
if(!b.drawing||b.drawing.pointerId!==e.pointerId){release();return}
if(flush==='settled'&&reconnectOpenGateOnRelease(b)){release(true);return}
if(discardUnmovedCreatedPath(b)){release(true);return}
const finishedPathIndex=b.drawing.pathIndex;b.drawing=null;b.armedGate=null;refreshInteractionState();
const removedWarpKnobPaths=sanitizeStateForPuzzle(b.meta,{quiet:true});
changed(b.id);if(removedWarpKnobPaths)toast('両端がワープセルにある未接続の線を削除しました。');
renderBoardNow(b);scheduleBoardCommandSettlement(b,{paint:true,invalidate:true,pathIndex:finishedPathIndex});
release();
}finally{perfEnd('pickupPointerFinish',started)}
};
b.svg.addEventListener('pointerup',e=>finishPointer(e,true));
b.svg.addEventListener('pointercancel',e=>finishPointer(e,false));
b.svg.addEventListener('lostpointercapture',e=>{if(b.pendingClaimPointer?.pointerId===e.pointerId){if(!b.pendingClaimPointer.released)clearPendingClaimPointer(b,e.pointerId,{release:false,releaseGesture:true})}else if(b.releaseDrain?.pointerId===e.pointerId)return;else if(b.drawing?.pointerId===e.pointerId){if(!recoverLostBoardPointerCapture(b,e))finishPointer(e,false)}else{gestureCoordinator.release(e.pointerId,'draw','lost-capture');refreshInteractionState()}});
b.svg.addEventListener('contextmenu',e=>e.preventDefault());
b.svg.addEventListener('focusin',()=>selectBoard(b));
b.svg.addEventListener('keydown',async e=>{
if(document.querySelector('#modal').classList.contains('show')||document.querySelector('#storeModal').classList.contains('show')||document.querySelector('#inventoryModal').classList.contains('show')||document.querySelector('#timeAttackModal').classList.contains('show'))return;
const st=metaState(b.id);if(st.solved)return;
const gateTarget=e.target.closest?.('.gate-hit'),endpointTarget=e.target.closest?.('.endpoint-hit'),claimKey=e.key==='Enter'||e.key===' '||e.key==='Backspace'||['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key);
if(claimKey&&!await ensureBoardClaimForInput(b))return;if(claimKey)touchBoardClaim(b.id);
if((e.key==='Enter'||e.key===' ')&&gateTarget){
e.preventDefault();selectBoard(b);startGate(b,Number(gateTarget.dataset.gate),null);activateBoardHud(b);b.svg.focus({preventScroll:true});return;
}
if((e.key==='Enter'||e.key===' ')&&endpointTarget){
e.preventDefault();selectBoard(b);const pi=Number(endpointTarget.dataset.pathIndex),side=endpointTarget.dataset.endpointSide||'end';
if(st.paths[pi]?.endGate==null){if(st.paths[pi].detachedStart)orientDetachedEndpoint(b,pi,side,null,null);else b.drawing=drawingForPath(b,pi,null,null);b.drawing.keyboardActive=true;activateBoardHud(b);renderBoard(b);b.svg.focus({preventScroll:true})}return;
}
const moves={ArrowUp:[-1,0],ArrowDown:[1,0],ArrowLeft:[0,-1],ArrowRight:[0,1]};
if(moves[e.key]&&activePath(b)){
e.preventDefault();b.drawing.keyboardActive=true;const path=activePath(b),last=path.cells[path.cells.length-1],move=moves[e.key];
extendOne(b,[last[0]+move[0],last[1]+move[1]]);b.svg.focus({preventScroll:true});return;
}
if(e.key==='Backspace'&&activePath(b)){
e.preventDefault();b.drawing.keyboardActive=true;const path=activePath(b);
if(path.cells.length>1)extendOne(b,path.cells[path.cells.length-2]);
else toast('\u59cb\u70b9\u306f\u524a\u9664\u3067\u304d\u307e\u305b\u3093\u3002');
}
});
}
let renderOriginX=0,renderOriginY=0;
let viewportRectCache=null,viewportResizeFrame=0;
function getViewportRect(force=false){if(force||!viewportRectCache)viewportRectCache=viewport.getBoundingClientRect();return viewportRectCache}
function getMinimapRect(force=false){if(force||!minimapRectCache)minimapRectCache=minimapCanvas.getBoundingClientRect();return minimapRectCache}
function boardScreenRect(board){
const viewportRect=getViewportRect(),scale=cam.scale,left=viewportRect.left+cam.x+((board.meta.x-renderOriginX)*UNIT-PAD)*scale,top=viewportRect.top+cam.y+((board.meta.y-renderOriginY)*UNIT-PAD)*scale;
return{left,top,right:left+board.w*scale,bottom:top+board.h*scale,width:board.w*scale,height:board.h*scale};
}
function boardWorldBounds(meta){const p=puzzleOf(meta);return{x:(meta.x-renderOriginX)*UNIT-PAD,y:(meta.y-renderOriginY)*UNIT-PAD,w:p.bounds.w*CELL+PAD*2,h:p.bounds.h*CELL+PAD*2}}
function viewportChunkBounds(screenMargin=0){
const r=getViewportRect(),margin=screenMargin/Math.max(MIN_CAMERA_SCALE,cam.scale),left=-cam.x/cam.scale-margin,top=-cam.y/cam.scale-margin,right=-cam.x/cam.scale+r.width/cam.scale+margin,bottom=-cam.y/cam.scale+r.height/cam.scale+margin;
return{minX:renderOriginX+Math.floor(left/UNIT)-1,maxX:renderOriginX+Math.ceil(right/UNIT)+1,minY:renderOriginY+Math.floor(top/UNIT)-1,maxY:renderOriginY+Math.ceil(bottom/UNIT)+1};
}
function visibleMetaIdsForBounds(bounds){const ids=new Set();for(let y=bounds.minY;y<=bounds.maxY;y++)for(let x=bounds.minX;x<=bounds.maxX;x++){const id=occupancy.get(key2(x,y));if(id)ids.add(id)}return ids}
const pendingVisibleHydrations=new Set();
function visibleMetaIds(){
const ids=visibleMetaIdsForBounds(viewportChunkBounds(24));
for(const[id,b]of rendered)if(b.drawing?.pointerId!=null)ids.add(id);return ids;
}
function scheduleWorldOverview(markDirty=false,{allowDuringInteraction=false}={}){
if(markDirty)overviewDirty=true;
if(allowDuringInteraction)overviewAllowInteractionBuild=true;
if(interactionActive('overview')&&!overviewAllowInteractionBuild)return;
if(!overviewCanvas||!inWorldOverview()||!overviewDirty||overviewFrame||overviewDelayTimer)return;
const step=timestamp=>{
if(!overviewFrame)return;
if(overviewAllowInteractionBuild&&timestamp-overviewInteractionLastBuild<OVERVIEW_INTERACTION_REBUILD_INTERVAL){overviewFrame=requestAnimationFrame(step);return}
if(timestamp-overviewLastDraw<AUXILIARY_FRAME_INTERVAL-1){overviewFrame=requestAnimationFrame(step);return}
overviewFrame=0;overviewLastDraw=timestamp;markVisualFrame(timestamp);
const rebuild=()=>{
overviewDelayTimer=0;const allow=overviewAllowInteractionBuild;overviewAllowInteractionBuild=false;
if(interactionActive('overview')&&!allow)return;
drawWorldOverview({allowDuringInteraction:allow});if(allow)overviewInteractionLastBuild=perfNow();
};
overviewDelayTimer=typeof requestIdleCallback==='function'?requestIdleCallback(rebuild,{timeout:180}):setTimeout(rebuild,0);
};
overviewFrame=requestAnimationFrame(step);
}
function overviewCacheNeedsInteractionRebuild(){
if(!overviewCache)return true;
const rect=getViewportRect(),width=Math.max(1,Math.round(rect.width)),height=Math.max(1,Math.round(rect.height)),[centerX,centerY]=cameraCenterInChunks(),scaleRatio=cam.scale/Math.max(MIN_CAMERA_SCALE,overviewCache.scale);
return overviewCache.revision!==minimapWorldRevision||overviewCache.width!==width||overviewCache.height!==height||
Math.abs(centerX-overviewCache.anchorX)*overviewCache.unit*Math.max(.01,scaleRatio)>overviewCache.overscan*.82||
Math.abs(centerY-overviewCache.anchorY)*overviewCache.unit*Math.max(.01,scaleRatio)>overviewCache.overscan*.82;
}
function positionCachedWorldOverview(){
if(!overviewCanvas||!overviewCache||!inWorldOverview())return false;
const rect=getViewportRect(),width=Math.max(1,Math.round(rect.width)),height=Math.max(1,Math.round(rect.height)),[centerX,centerY]=cameraCenterInChunks(),ratio=Math.max(.01,cam.scale/Math.max(MIN_CAMERA_SCALE,overviewCache.scale)),
left=width/2-(overviewCache.baseWidth/2+(centerX-overviewCache.anchorX)*overviewCache.unit)*ratio,
top=height/2-(overviewCache.baseHeight/2+(centerY-overviewCache.anchorY)*overviewCache.unit)*ratio;
overviewCanvas.hidden=false;overviewCanvas.style.transform=`translate3d(${left.toFixed(2)}px,${top.toFixed(2)}px,0) scale(${ratio})`;
perfCount('overviewTransformUpdates');return true;
}
function rebuildWorldOverviewCache(width,height,dpr,centerX,centerY){
if(interactionActive('overview'))perfCount('overviewBuildsDuringInteraction');
const started=perfStart(),unit=UNIT*cam.scale,overscan=Math.max(96,Math.min(OVERVIEW_CACHE_OVERSCAN_PX,Math.max(width,height)*.18)),baseWidth=width+overscan*2,baseHeight=height+overscan*2,
pixelWidth=Math.max(1,Math.round(baseWidth*dpr)),pixelHeight=Math.max(1,Math.round(baseHeight*dpr));
if(overviewBase.width!==pixelWidth||overviewBase.height!==pixelHeight){overviewBase.width=pixelWidth;overviewBase.height=pixelHeight}
const context=overviewBase.getContext('2d',{alpha:false});context.setTransform(dpr,0,0,dpr,0,0);context.fillStyle='#101315';context.fillRect(0,0,baseWidth,baseHeight);
const halfX=baseWidth/(2*unit),halfY=baseHeight/(2*unit),bounds={minX:Math.floor(centerX-halfX)-1,maxX:Math.ceil(centerX+halfX)+1,minY:Math.floor(centerY-halfY)-1,maxY:Math.ceil(centerY+halfY)+1},ids=visibleMetaIdsForBounds(bounds),visibleMetas=[...ids].map(id=>data.metas[id]).filter(Boolean),
mapX=value=>baseWidth/2+(value-centerX)*unit,mapY=value=>baseHeight/2+(value-centerY)*unit;
drawMapBoardCells(context,visibleMetas,mapX,mapY,unit,{showLevels:true});const longSegments=drawMapLongLines(context,visibleMetas,ids,mapX,mapY,Math.max(1,cam.scale*1.5)),storeCount=drawMapStores(context,visibleMetas,mapX,mapY,unit);
overviewCache={revision:minimapWorldRevision,width,height,dpr,scale:cam.scale,anchorX:centerX,anchorY:centerY,unit,overscan,baseWidth,baseHeight,longSegments,storeCount,boardCount:ids.size};
perfCount('overviewCacheBuilds');perfGauge('overviewBoards',ids.size);perfGauge('overviewPaths',longSegments);perfGauge('overviewShops',storeCount);perfEnd('rebuildWorldOverviewCache',started);
}
function drawWorldOverview({allowDuringInteraction=false}={}){
const started=perfStart();if(!overviewCanvas||!inWorldOverview())return;
const interacting=interactionActive('overview');if(interacting&&!allowDuringInteraction)return;
const rect=getViewportRect(),width=Math.max(1,Math.round(rect.width)),height=Math.max(1,Math.round(rect.height)),dpr=1,[centerX,centerY]=cameraCenterInChunks(),scaleRatio=overviewCache?cam.scale/Math.max(MIN_CAMERA_SCALE,overviewCache.scale):1;
const stale=!overviewCache||overviewCache.revision!==minimapWorldRevision||overviewCache.width!==width||overviewCache.height!==height||overviewCache.dpr!==dpr||(!interacting&&Math.abs(overviewCache.scale-cam.scale)>1e-6)||
Math.abs(centerX-overviewCache.anchorX)*overviewCache.unit*Math.max(.01,scaleRatio)>overviewCache.overscan*.82||Math.abs(centerY-overviewCache.anchorY)*overviewCache.unit*Math.max(.01,scaleRatio)>overviewCache.overscan*.82;
if(stale){
rebuildWorldOverviewCache(width,height,dpr,centerX,centerY);
const bitmapWidth=Math.max(1,Math.round(overviewCache.baseWidth*dpr)),bitmapHeight=Math.max(1,Math.round(overviewCache.baseHeight*dpr));
if(overviewCanvas.width!==bitmapWidth||overviewCanvas.height!==bitmapHeight){overviewCanvas.width=bitmapWidth;overviewCanvas.height=bitmapHeight}
const context=overviewCanvas.getContext('2d',{alpha:false});context.setTransform(1,0,0,1,0,0);context.drawImage(overviewBase,0,0,bitmapWidth,bitmapHeight);
overviewCanvas.style.width=`${overviewCache.baseWidth}px`;overviewCanvas.style.height=`${overviewCache.baseHeight}px`;perfCount('overviewBitmapCopies');
}
overviewCanvas.hidden=false;overviewDirty=false;positionCachedWorldOverview();
perfGauge('overviewBoards',overviewCache.boardCount);perfGauge('overviewPaths',overviewCache.longSegments);perfGauge('overviewShops',0);perfEnd('drawWorldOverview',started);
}
async function hydrateVisibleMetas(){
if(inWorldOverview())return 0;const queue=[];
for(const id of visibleMetaIds()){const meta=data.metas[id];if(!meta||meta.puzzle||pendingVisibleHydrations.has(id))continue;pendingVisibleHydrations.add(id);queue.push(meta)}
let cursor=0,completed=0;async function worker(){while(cursor<queue.length){const meta=queue[cursor++];try{await hydrateMeta(meta);completed++}catch(error){data.quarantine[meta.id]={failedAt:trustedNow(),message:String(error?.message||error),retries:(data.quarantine[meta.id]?.retries||0)+1}}finally{pendingVisibleHydrations.delete(meta.id)}}}
await Promise.all(Array.from({length:Math.min(HYDRATE_CONCURRENCY,queue.length)},worker));return completed;
}
let lodFrame=0;
let lodIdleHandle=0;
function scheduleLodPass(){if(lodFrame||lodIdleHandle)return;const run=()=>{lodFrame=0;lodIdleHandle=0;ensureBoards()},interacting=interactionActive('world');if(interacting&&typeof requestIdleCallback==='function')lodIdleHandle=requestIdleCallback(run,{timeout:500});else if(interacting)lodIdleHandle=setTimeout(run,180);else lodFrame=requestAnimationFrame(run)}
function desiredInteractiveBoardIds(ids){
const desired=new Set();
for(const id of ids)if(data.metas[id]?.puzzle)desired.add(id);
return desired;
}
function ensureBoards(){
const started=perfStart();perfCount('ensureBoardsRuns');
if(interactionActive('world')){perfCount('lodPassesDeferredDuringInteraction');scheduleLodPass();if(inWorldOverview())scheduleWorldOverview(true);perfEnd('ensureBoards',started);return}
let changes=0,pending=false;
if(inWorldOverview()){
for(const[,b]of rendered)if(b.drawing?.pointerId==null){if(changes<LOD_CHANGES_PER_PASS){destroyBoard(b);changes++}else pending=true}
if(pending)scheduleLodPass();
scheduleWorldOverview(true);perfEnd('ensureBoards',started);return;
}
if([...visibleMetaIds()].some(id=>data.metas[id]&&!data.metas[id].puzzle))void hydrateVisibleMetas().then(count=>{if(count)ensureBoards()});
const ids=visibleMetaIds(),desiredInteractive=desiredInteractiveBoardIds(ids);
for(const id of desiredInteractive){
const meta=data.metas[id];if(!meta?.puzzle||rendered.has(id))continue;
if(changes<LOD_CHANGES_PER_PASS){makeBoard(meta);changes++}else pending=true;
}
for(const[id,b]of rendered)if(!data.metas[id]||(b.drawing?.pointerId==null&&!ids.has(id))){if(changes<LOD_CHANGES_PER_PASS){destroyBoard(b);changes++}else pending=true}
if(pending)scheduleLodPass();
const visibleUnsolvedBoards=[...ids].filter(id=>data.metas[id]?.puzzle&&!metaState(id).solved).length;
perfGauge('visibleBoardIds',ids.size);perfGauge('visibleUnsolvedBoards',visibleUnsolvedBoards);perfGauge('interactiveBoardBudget',desiredInteractive.size);perfEnd('ensureBoards',started);
}
function renderAll(){for(const b of rendered.values())renderBoard(b)}
let cam={x:0,y:0,scale:1},pan=null,pinch=null,cameraFrame=0,cameraFrameDelayTimer=0,cameraLastDraw=0,pendingCameraInteraction=null,visibilityTimer=0,previousViewportSize=null;
function currentCameraAnchor(){const r=getViewportRect(),scale=Math.max(MIN_CAMERA_SCALE,Math.min(1.8,cam.scale||1));return{centerX:renderOriginX+(r.width/2-cam.x)/(UNIT*scale),centerY:renderOriginY+(r.height/2-cam.y)/(UNIT*scale),scale}}
function recordCameraAnchor(){
const next=currentCameraAnchor(),previous=data.cameraAnchor;
if(previous&&Math.abs(previous.centerX-next.centerX)<1e-9&&Math.abs(previous.centerY-next.centerY)<1e-9&&Math.abs(previous.scale-next.scale)<1e-9)return false;
data.cameraAnchor=next;markGlobalDirty(false);save();return true;
}
function restoreSavedCamera(){const anchor=data.cameraAnchor;if(!anchor||!Number.isFinite(anchor.centerX)||!Number.isFinite(anchor.centerY)||!Number.isFinite(anchor.scale))return false;const r=getViewportRect();cam.scale=Math.max(MIN_CAMERA_SCALE,Math.min(1.8,anchor.scale));cam.x=r.width/2-(anchor.centerX-renderOriginX)*UNIT*cam.scale;cam.y=r.height/2-(anchor.centerY-renderOriginY)*UNIT*cam.scale;applyCamera(true);previousViewportSize={width:r.width,height:r.height};return true}
const touchPoints=new Map();
function rebaseWorldOrigin(){
const r=getViewportRect(),worldX=renderOriginX+(r.width/2-cam.x)/(UNIT*cam.scale),worldY=renderOriginY+(r.height/2-cam.y)/(UNIT*cam.scale);
if(Math.abs(worldX-renderOriginX)<2048&&Math.abs(worldY-renderOriginY)<2048)return;
const nextX=Math.trunc(worldX),nextY=Math.trunc(worldY),dx=nextX-renderOriginX,dy=nextY-renderOriginY;renderOriginX=nextX;renderOriginY=nextY;cam.x+=dx*UNIT*cam.scale;cam.y+=dy*UNIT*cam.scale;
for(const b of rendered.values()){b.card.style.left=((b.meta.x-renderOriginX)*UNIT-PAD)+'px';b.card.style.top=((b.meta.y-renderOriginY)*UNIT-PAD)+'px'}overviewDirty=true;
}
function repositionActiveBoardHud(){const board=rendered.get(hudBoardId);if(board?.card?.isConnected&&boardPlayHudVisible(board))positionBoardLabel(board)}
function applyCamera(immediate=false,frameTimestamp=null){
const cameraGestureActive=Boolean(pan||pinch||interactionActive('camera'));if(!cameraGestureActive)rebaseWorldOrigin();const overview=updateZoomPresentation();
const paint=timestamp=>{if(!immediate&&cameraLastDraw&&timestamp-cameraLastDraw<GLOBAL_FRAME_INTERVAL-1){cameraFrame=requestAnimationFrame(paint);return}cameraFrame=0;cameraFrameDelayTimer=0;cameraLastDraw=timestamp;markVisualFrame(timestamp);if(overview){positionCachedWorldOverview();if(overviewCacheNeedsInteractionRebuild()){overviewDirty=true;scheduleWorldOverview(false,{allowDuringInteraction:cameraGestureActive})}}else world.style.transform=`translate3d(${cam.x}px,${cam.y}px,0) scale(${cam.scale})`;repositionActiveBoardHud()};
if(immediate){if(cameraFrame)cancelAnimationFrame(cameraFrame);if(cameraFrameDelayTimer)clearTimeout(cameraFrameDelayTimer);paint(frameTimestamp??perfNow())}
else if(!cameraFrame)cameraFrame=requestAnimationFrame(paint);
if(overview){overviewDirty=true;if(!cameraGestureActive)scheduleWorldOverview()}
if(cameraGestureActive){if(visibilityTimer){clearTimeout(visibilityTimer);visibilityTimer=0}}else{clearTimeout(visibilityTimer);visibilityTimer=setTimeout(()=>{visibilityTimer=0;ensureBoards();repositionActiveBoardHud()},90)}
minimapDirty=true;
if(cameraGestureActive)shiftOnlineLayersForCamera();else{scheduleMinimap();redrawOnlineLayersAfterCamera()}
scheduleRealtimeViewport();
}
function zoomAt(clientX,clientY,nextScale,{relative=false,inputAt=perfNow()}={}){
const base=pendingCameraInteraction||cam,r=getViewportRect(),mx=clientX-r.left,my=clientY-r.top,wx=(mx-base.x)/base.scale,wy=(my-base.y)/base.scale,
scale=Math.max(MIN_CAMERA_SCALE,Math.min(1.8,relative?base.scale*nextScale:nextScale));
queueCameraInteraction({scale,x:mx-wx*scale,y:my-wy*scale,inputAt});
}
function centerMeta(meta,{preserveScale=false,select=true}={}){
if(!meta?.puzzle)return false;
const bounds=boardWorldBounds(meta),r=getViewportRect();
worldOverviewActive=false;if(!preserveScale)cam.scale=Math.min(1.15,Math.max(.55,Math.min((r.width-90)/bounds.w,(r.height-90)/bounds.h)));
cam.x=r.width/2-(bounds.x+bounds.w/2)*cam.scale;cam.y=r.height/2-(bounds.y+bounds.h/2)*cam.scale;updateZoomPresentation(true);applyCamera();
previousViewportSize={width:r.width,height:r.height};ensureBoards();const board=rendered.get(meta.id);if(select&&board)selectBoard(board);recordCameraAnchor();return true;
}
function centerOrigin(){return centerMeta(data.metas.B0||Object.values(data.metas).find(meta=>meta.puzzle))}
async function randomUnsolvedMeta(){
let candidates=Object.values(data.metas).filter(meta=>meta&&!metaState(meta.id).solved);
if(!candidates.length)candidates=Object.values(data.metas).filter(Boolean);
if(!candidates.length)return null;
const random=new Uint32Array(1);try{if(!globalThis.crypto?.getRandomValues)throw new Error('secure random unavailable');globalThis.crypto.getRandomValues(random)}catch(_){random[0]=Math.floor(Math.random()*0xffffffff)}
const meta=candidates[random[0]%candidates.length];
if(meta&&!meta.puzzle)await hydrateMeta(meta);
return meta;
}
async function centerRandomBoard(){
let candidates=Object.values(data.metas).filter(Boolean);
if(candidates.length>1)candidates=candidates.filter(meta=>meta.id!==activeBoard);
if(!candidates.length){toast('\u79fb\u52d5\u3067\u304d\u308b\u76e4\u9762\u306f\u3042\u308a\u307e\u305b\u3093\u3002');return false}
const random=new Uint32Array(1);try{if(!globalThis.crypto?.getRandomValues)throw new Error('secure random unavailable');globalThis.crypto.getRandomValues(random)}catch(_){random[0]=Math.floor(Math.random()*0xffffffff)}
const meta=candidates[random[0]%candidates.length];
if(!meta.puzzle)try{await hydrateMeta(meta)}catch(error){toast('\u76e4\u9762\u3092\u8aad\u307f\u8fbc\u3081\u307e\u305b\u3093\u3002');return false}
return centerMeta(meta);
}
function cancelPointerGestures(){
for(const b of rendered.values())if(b.drawing?.pointerId!=null){const pointerId=b.drawing.pointerId;cancelBoardDragFrame(b);safeRelease(b.svg,pointerId);if(discardUnmovedCreatedPath(b)){renderBoard(b);continue}b.drawing.pointerId=null;b.drawing.lastPoint=null;b.drawing.pointerOffset=null;clearDrawingPointerState(b.drawing);changed(b.id);renderBoard(b)}
refreshInteractionState();
}
function cancelCameraGestures(reason='cancelled'){
if(pendingCameraInteraction)flushCameraInteraction();
const pointerIds=new Set(touchPoints.keys());if(pan?.id!=null)pointerIds.add(pan.id);
for(const pointerId of pointerIds){gestureCoordinator.release(pointerId,null,reason);try{if(viewport.hasPointerCapture?.(pointerId))viewport.releasePointerCapture(pointerId)}catch(_){}}
touchPoints.clear();pan=null;pinch=null;viewport.classList.remove('panning');
}
function cancelMinimapGesture(reason='cancelled'){
const pointerId=minimapPointerState?.id;if(pointerId==null)return;
minimapPointerState=null;gestureCoordinator.release(pointerId,'minimap',reason);interactionState.clear('minimap');
try{if(minimapCanvas.hasPointerCapture?.(pointerId))minimapCanvas.releasePointerCapture(pointerId)}catch(_){}
}
function cancelAllPointerInteractions(reason='cancelled'){
cancelReactionGesture();cancelPointerGestures();cancelCameraGestures(reason);cancelMinimapGesture(reason);gestureCoordinator.cancelAll(reason);
interactionState.clear('reaction');interactionState.clear('minimap');interactionState.clear('wheel');refreshInteractionState(false);scheduleInteractionSettlePresentation();
}
function touchPair(){
const points=[...touchPoints.values()];if(points.length<2)return null;
const[a,b]=points,center=[(a.x+b.x)/2,(a.y+b.y)/2],distance=Math.hypot(a.x-b.x,a.y-b.y);
return{center,distance};
}
function commitCameraInteraction(next,timestamp=perfNow()){
const started=perfStart();if(pendingCameraInteraction===next)pendingCameraInteraction=null;if(!next){perfEnd('commitCameraInteraction',started);return false}
cam.x=next.x;cam.y=next.y;if(Number.isFinite(next.scale))cam.scale=next.scale;applyCamera(true,timestamp);recordInteractionCommit('camera',timestamp,next.inputAt);perfCount('cameraInteractionFrames');const workDuration=perfEnd('commitCameraInteraction',started);observeInteractionFrame(timestamp,workDuration);return true;
}
const cameraInteractionScheduler=FrameSchedulerApi.createFrameScheduler({
interval:DRAG_FRAME_INTERVAL,
tolerance:INTERACTION_FRAME_TOLERANCE_MS,
watchdogDelay:CAMERA_DISPLAY_WATCHDOG_MS,
now:perfNow,
onWatchdog:()=>perfCount('cameraDisplayWatchdogFrames'),
commit:commitCameraInteraction
});
function queueCameraInteraction(next){
pendingCameraInteraction=next;cameraInteractionScheduler.push(next);
}
function flushCameraInteraction(){
const committed=cameraInteractionScheduler.flush();recordCameraAnchor();return committed;
}
function leftFieldPanAllowed(event){
if(event.button!==0)return false;
const target=event.target;if(target?.closest?.('button,a,input,select,textarea,[role="button"]'))return false;
const card=target?.closest?.('.board-card');if(!card)return true;
const id=card.dataset?.id;
return card.classList?.contains('solved')||Boolean(id&&data.metas[id]&&metaState(id).solved);
}
function beginPan(e){
if(e.target===minimapCanvas||e.target?.closest?.('#minimapCanvas')||gestureCoordinator.owner(e.pointerId)&&!gestureCoordinator.owns(e.pointerId,'pan'))return;
if(e.button===0&&inWorldOverview()){
const rect=getViewportRect(),worldX=renderOriginX+(e.clientX-rect.left-cam.x)/(UNIT*cam.scale),worldY=renderOriginY+(e.clientY-rect.top-cam.y)/(UNIT*cam.scale),storeMeta=nearestStoreMetaAtWorldPoint(worldX,worldY,18,UNIT*cam.scale);
if(storeMeta){e.preventDefault();e.stopPropagation();playSound('shop');openStoreMeta(storeMeta);return}
}
if(e.pointerType==='touch'){
touchPoints.set(e.pointerId,{x:e.clientX,y:e.clientY});
if(touchPoints.size===2){
e.preventDefault();e.stopPropagation();cancelPointerGestures();
for(const pointerId of touchPoints.keys())gestureCoordinator.claim(pointerId,'pinch',{replace:true});
const pair=touchPair(),r=getViewportRect(),mx=pair.center[0]-r.left,my=pair.center[1]-r.top;
pinch={startDistance:Math.max(1,pair.distance),startScale:cam.scale,worldX:(mx-cam.x)/cam.scale,worldY:(my-cam.y)/cam.scale,lastCenter:pair.center,lastAt:performance.now()};
pan=null;viewport.classList.add('panning');refreshInteractionState();return;
}
if(touchPoints.size===1&&!e.target.closest?.('.board-svg,button,a,input,select,textarea,[role="button"]')){
if(!gestureCoordinator.claim(e.pointerId,'pan')){touchPoints.delete(e.pointerId);return}
e.preventDefault();e.stopPropagation();pan={id:e.pointerId,x:e.clientX,y:e.clientY,cx:cam.x,cy:cam.y,lastX:e.clientX,lastY:e.clientY,lastAt:performance.now(),touch:true};viewport.classList.add('panning');
try{viewport.setPointerCapture(e.pointerId)}catch(_){}refreshInteractionState();
}
return;
}
if(e.button!==2&&!leftFieldPanAllowed(e))return;
if(!gestureCoordinator.claim(e.pointerId,'pan'))return;
e.preventDefault();e.stopPropagation();pan={id:e.pointerId,x:e.clientX,y:e.clientY,cx:cam.x,cy:cam.y,lastX:e.clientX,lastY:e.clientY,lastAt:performance.now(),touch:false,button:e.button};viewport.classList.add('panning');
try{viewport.setPointerCapture(e.pointerId)}catch(_){}refreshInteractionState();
}
function movePan(e){
if(e.pointerType==='touch'&&touchPoints.has(e.pointerId))touchPoints.set(e.pointerId,{x:e.clientX,y:e.clientY});
if(pinch&&touchPoints.size>=2){
e.preventDefault();e.stopPropagation();const pair=touchPair(),r=getViewportRect(),mx=pair.center[0]-r.left,my=pair.center[1]-r.top,
nextScale=Math.max(.35,Math.min(1.8,pinch.startScale*pair.distance/pinch.startDistance));
queueCameraInteraction({scale:nextScale,x:mx-pinch.worldX*nextScale,y:my-pinch.worldY*nextScale,inputAt:Number(e.timeStamp)||perfNow()});return;
}
if(!pan||pan.id!==e.pointerId)return;
e.preventDefault();if(pan.touch)e.stopPropagation();
queueCameraInteraction({scale:cam.scale,x:pan.cx+e.clientX-pan.x,y:pan.cy+e.clientY-pan.y,inputAt:Number(e.timeStamp)||perfNow()});
}
function stopPan(e){
touchPoints.delete(e.pointerId);
let ended=false;
if(pinch&&touchPoints.size<2){flushCameraInteraction();pinch=null;ended=true;gestureCoordinator.release(e.pointerId,'pinch');for(const pointerId of touchPoints.keys())gestureCoordinator.release(pointerId,'pinch')}
if(pan&&pan.id===e.pointerId){flushCameraInteraction();pan=null;ended=true;gestureCoordinator.release(e.pointerId,'pan');try{viewport.releasePointerCapture(e.pointerId)}catch(_){}}
if(ended){viewport.classList.remove('panning');refreshInteractionState();clearTimeout(visibilityTimer);visibilityTimer=setTimeout(()=>{visibilityTimer=0;rebaseWorldOrigin();ensureBoards();repositionActiveBoardHud();scheduleWorldOverview(true)},60);scheduleMinimap(true);redrawOnlineLayersAfterCamera();scheduleRealtimeViewport(true)}
}
let tooltipFrame=0,pendingTooltipEvent=null,tooltipSize=null,tooltipText='',tooltipLastProbe=0;
function hideSpecialTooltip(){pendingTooltipEvent=null;if(tooltipFrame){cancelAnimationFrame(tooltipFrame);tooltipFrame=0}if(specialTooltip)specialTooltip.hidden=true}
function positionSpecialTooltip(event){
if(!specialTooltip||specialTooltip.hidden)return;
const view=globalThis.visualViewport,gap=14,pad=10,viewLeft=view?.offsetLeft||0,viewTop=view?.offsetTop||0,viewWidth=view?.width||innerWidth,viewHeight=view?.height||innerHeight,
minLeft=viewLeft+pad,minTop=viewTop+pad,maxRight=viewLeft+viewWidth-pad,maxBottom=viewTop+viewHeight-pad;
if(!tooltipSize)tooltipSize={width:specialTooltip.offsetWidth||260,height:specialTooltip.offsetHeight||42};
const{width,height}=tooltipSize;
let left=event.clientX+gap,top=event.clientY+gap;
if(left+width>maxRight)left=event.clientX-width-gap;
if(top+height>maxBottom)top=event.clientY-height-gap;
left=Math.max(minLeft,Math.min(left,maxRight-width));top=Math.max(minTop,Math.min(top,maxBottom-height));
specialTooltip.style.left=left+'px';specialTooltip.style.top=top+'px';
}
function specialInfoFromEvent(event){
const card=event.target?.closest?.('.board-card'),b=card?rendered.get(card.dataset.id):null;if(!b||metaState(b.id).solved)return null;
let cell=null;
const directCell=event.target?.closest?.('[data-cell]')?.dataset.cell;
if(directCell){const parts=directCell.split(',').map(Number);if(parts.length===2&&parts.every(Number.isFinite))cell=parts}
if(!cell){const directGate=Number(event.target?.closest?.('.gate-hit')?.dataset.gate);if(Number.isInteger(directGate)&&b.p.g[directGate]){const gate=gateObj(b.p,directGate);cell=[gate.cell[0],gate.cell[1]]}}
const point=eventToSvg(b,event);
if(!cell)cell=cellAt(b,point);
let info=cell?b.specialInfo?.get(ckey(...cell)):null;
if(!info&&point&&b.specialInfo?.size){
let nearest=null,nearestDistance=(CELL*.8)**2;
for(const[key,candidate]of b.specialInfo){const rc=key.split(',').map(Number),center=boardCellCenter(rc),distance=(center[0]-point[0])**2+(center[1]-point[1])**2;if(distance<=nearestDistance){nearestDistance=distance;nearest={cell:rc,info:candidate}}}
if(nearest){cell=nearest.cell;info=nearest.info}
}
return info?{b,cell,info}:null;
}
function updateSpecialTooltip(event){if(pan||pinch){hideSpecialTooltip();return}const target=specialInfoFromEvent(event);if(!target||!specialTooltip){hideSpecialTooltip();return}if(tooltipText!==target.info.description){tooltipText=target.info.description;specialTooltip.textContent=tooltipText;tooltipSize=null}specialTooltip.hidden=false;positionSpecialTooltip(event)}
function queueSpecialTooltip(event){
if(pan||pinch||document.body.classList.contains('is-drawing')){if(!specialTooltip?.hidden)hideSpecialTooltip();return}
pendingTooltipEvent={target:event.target,clientX:event.clientX,clientY:event.clientY};
if(tooltipFrame)return;
const step=timestamp=>{if(timestamp-tooltipLastProbe<80){tooltipFrame=requestAnimationFrame(step);return}tooltipFrame=0;tooltipLastProbe=timestamp;const pending=pendingTooltipEvent;pendingTooltipEvent=null;if(pending)updateSpecialTooltip(pending)};
tooltipFrame=requestAnimationFrame(step);
}
viewport.addEventListener('pointermove',queueSpecialTooltip,{passive:true});
viewport.addEventListener('pointerleave',hideSpecialTooltip,{passive:true});
viewport.addEventListener('pointerdown',hideSpecialTooltip,true);
viewport.addEventListener('pointerdown',beginReactionGesture,true);
viewport.addEventListener('pointerdown',beginPan,true);
window.addEventListener('pointermove',moveReactionGesture,true);
window.addEventListener('pointermove',movePan,true);
window.addEventListener('pointerup',endReactionGesture,true);
window.addEventListener('pointerup',stopPan,true);
window.addEventListener('pointercancel',cancelReactionGesture,true);
window.addEventListener('pointercancel',stopPan,true);
window.addEventListener('blur',()=>cancelAllPointerInteractions('blur'));
viewport.addEventListener('lostpointercapture',stopPan,true);
viewport.addEventListener('contextmenu',e=>e.preventDefault());
viewport.addEventListener('wheel',e=>{e.preventDefault();pulseWheelInteraction();zoomAt(e.clientX,e.clientY,Math.exp(-e.deltaY*.0012),{relative:true,inputAt:Number(e.timeStamp)||perfNow()})},{passive:false});
minimapCanvas.addEventListener('pointerdown',beginMinimapPointer);
minimapCanvas.addEventListener('pointermove',moveMinimapPointer);
minimapCanvas.addEventListener('pointerup',endMinimapPointer);
minimapCanvas.addEventListener('pointercancel',endMinimapPointer);
minimapCanvas.addEventListener('lostpointercapture',endMinimapPointer);
minimapCanvas.addEventListener('keydown',keyMinimap);
function handleViewportResize(){
viewportResizeFrame=0;viewportRectCache=null;minimapRectCache=null;const r=getViewportRect(true);
if(previousViewportSize){
const worldCenterX=(previousViewportSize.width/2-cam.x)/cam.scale,worldCenterY=(previousViewportSize.height/2-cam.y)/cam.scale;
cam.x=r.width/2-worldCenterX*cam.scale;cam.y=r.height/2-worldCenterY*cam.scale;
}
previousViewportSize={width:r.width,height:r.height};applyCamera();
}
function scheduleViewportResize(){viewportRectCache=null;minimapRectCache=null;if(!viewportResizeFrame)viewportResizeFrame=requestAnimationFrame(handleViewportResize)}
window.addEventListener('resize',scheduleViewportResize);
globalThis.visualViewport?.addEventListener?.('resize',scheduleViewportResize);
if(typeof ResizeObserver!=='undefined')new ResizeObserver(scheduleViewportResize).observe(viewport);
if(typeof ResizeObserver!=='undefined')new ResizeObserver(()=>{minimapRectCache=null;invalidateMinimapWorld()}).observe(minimapCanvas);
const modal=document.querySelector('#modal'),helpBtn=document.querySelector('#helpBtn'),helpPanel=modal.querySelector('.panel'),
storeModal=document.querySelector('#storeModal'),storePanel=storeModal.querySelector('.store-panel'),
storeTitle=document.querySelector('#storeTitle'),storeWallet=document.querySelector('#storeWallet'),
storeMeta=document.querySelector('#storeMeta'),storeInventory=document.querySelector('#storeInventory'),
inventoryModal=document.querySelector('#inventoryModal'),inventoryPanel=inventoryModal.querySelector('.inventory-panel'),
inventoryBtn=document.querySelector('#inventoryBtn'),inventoryCountEl=document.querySelector('#inventoryCount'),
inventoryTotal=document.querySelector('#inventoryTotal'),inventoryTarget=document.querySelector('#inventoryTarget'),
inventoryList=document.querySelector('#inventoryList'),
timeAttackModal=document.querySelector('#timeAttackModal'),timeAttackPanel=timeAttackModal.querySelector('.time-attack-panel'),
timeAttackBtn=document.querySelector('#timeAttackBtn'),timeAttackButtonLabel=document.querySelector('#timeAttackButtonLabel'),
timeAttackHeadingScore=document.querySelector('#timeAttackHeadingScore'),timeAttackSetup=document.querySelector('#timeAttackSetup'),
timeAttackLive=document.querySelector('#timeAttackLive'),timeAttackResultEl=document.querySelector('#timeAttackResult'),
timeAttackClockEl=document.querySelector('#timeAttackClock'),timeAttackCollectedEl=document.querySelector('#timeAttackCollected'),
timeAttackMultiplierEl=document.querySelector('#timeAttackMultiplier'),timeAttackProjectedEl=document.querySelector('#timeAttackProjected'),
timeAttackResultLimit=document.querySelector('#timeAttackResultLimit'),timeAttackResultSolves=document.querySelector('#timeAttackResultSolves'),
timeAttackResultTotal=document.querySelector('#timeAttackResultTotal'),timeAttackShareText=document.querySelector('#timeAttackShareText'),
copyTimeAttackBtn=document.querySelector('#copyTimeAttack'),closeTimeAttackBtn=document.querySelector('#closeTimeAttack'),
timeAttackCountdownOverlay=document.querySelector('#timeAttackCountdownOverlay'),timeAttackCountdownValue=timeAttackCountdownOverlay.querySelector('span'),
settingsModal=document.querySelector('#settingsModal'),settingsPanel=settingsModal.querySelector('.settings-panel'),settingsPlayerName=document.querySelector('#settingsPlayerName'),lightweightRenderingToggle=document.querySelector('#lightweightRenderingToggle'),soundEnabledToggle=document.querySelector('#soundEnabledToggle'),resetSettingsBtn=document.querySelector('#resetSettings'),closeSettingsBtn=document.querySelector('#closeSettings'),customEmojiCursor=document.querySelector('#customEmojiCursor'),pickupHandleOverlay=document.querySelector('#pickupHandleOverlay');
let openStoreBoardId=null;
let timeAttackTimer=null,timeAttackCountdownActive=false,timeAttackCountdownHideTimer=0,timeAttackFinalCountdownSecond=null,entryChoicePending=false;
function trapDialogFocus(panel,e){
if(e.key!=='Tab')return;
const focusable=[...panel.querySelectorAll('button,[href],[tabindex]:not([tabindex="-1"])')].filter(el=>!el.disabled&&!el.hidden);
if(!focusable.length){e.preventDefault();panel.focus();return}
const first=focusable[0],last=focusable[focusable.length-1];
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus()}
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus()}
}
function setDialogInert(root,inert){
root.inert=Boolean(inert);
if(inert)root.setAttribute('inert','');
else root.removeAttribute('inert');
}
function focusOutsideDialog(root,preferred=null){
const active=document.activeElement;
if(!active||!root.contains(active))return;
const target=preferred?.isConnected&&!preferred.disabled?preferred:viewport;
try{target?.focus?.({preventScroll:true})}catch(_){target?.focus?.()}
if(root.contains(document.activeElement))active.blur?.();
}
function openDialogRoot(root){
cancelAllPointerInteractions('modal');gestureCoordinator.claim(`dialog:${root.id||'root'}`,'dialog',{replace:true});interactionState.set('dialog',root.id||'root');
setDialogInert(root,false);
root.setAttribute('aria-hidden','false');
root.classList.add('show');
}
function closeDialogRoot(root,preferredFocus=null){
focusOutsideDialog(root,preferredFocus);
root.classList.remove('show');
setDialogInert(root,true);
root.setAttribute('aria-hidden','true');
gestureCoordinator.release(`dialog:${root.id||'root'}`,'dialog');interactionState.clear('dialog');
}
function debugAllItemsEnabled(){return DEBUG_PURCHASE_MODE}
function storeItemActive(item){return Boolean(item&&(item.cursorStyle?data.cursorStyle===item.cursorStyle:item.lineColor?data.lineColorStyle===item.id:item.lineEffect?data.lineEffectStyle===item.lineEffect:item.reactionStyle?data.reactionStyle===item.reactionStyle:item.scoreLens?data.scoreLensEnabled:false))}
function setItemIcon(node,item){
node.classList.toggle('emoji-glyph',Boolean(item?.cursorEmoji&&!item?.flagAsset));
node.classList.toggle('line-color-swatch',Boolean(item?.lineColor));node.classList.toggle('aurora-swatch',item?.aurora===true);
if(item?.lineColor)node.style.setProperty('--item-color',item.lineColor);else node.style.removeProperty('--item-color');
if(item?.flagAsset){
let image=node.firstElementChild?.matches?.('img.item-flag-image')?node.firstElementChild:null;
if(!image){image=document.createElement('img');image.className='item-flag-image';image.alt='';image.draggable=false;image.loading='lazy';image.decoding='async';image.fetchPriority='low';image.addEventListener('load',()=>perfCount('inventoryImagesDecoded'));node.replaceChildren(image);perfCount('inventoryImagesCreated')}
if(image.getAttribute('src')!==item.flagAsset)image.src=item.flagAsset;
}else if(node.textContent!==(item?.icon||''))node.textContent=item?.icon||'';
}
function panelScrollPosition(panel){return{top:Number(panel?.scrollTop)||0,left:Number(panel?.scrollLeft)||0}}
function restorePanelScroll(panel,position){if(!panel||!position)return;panel.scrollTop=position.top;panel.scrollLeft=position.left}
const inventoryCollapsedCategories=new Set(),inventoryCategoryViews=new Map(),inventoryItemViews=new Map(),
inventoryLineEffectItemIds=new Map(STORE_ITEMS.filter(item=>item.lineEffect).map(item=>[item.lineEffect,item.id])),
inventoryReactionStyleItemIds=new Map(STORE_ITEMS.filter(item=>item.reactionStyle).map(item=>[item.reactionStyle,item.id]));
let inventoryHasRendered=false,inventorySelectedCursorItemId=null;
function rememberInventoryCategoryState(){
for(const section of inventoryList?.querySelectorAll?.('details.inventory-section[data-category]')||[]){
if(section.open)inventoryCollapsedCategories.delete(section.dataset.category);else inventoryCollapsedCategories.add(section.dataset.category);
}
}
function createInventoryCategoryView(category){
const section=document.createElement('details'),title=document.createElement('summary'),titleText=document.createTextNode(category.title),countBadge=document.createElement('span'),list=document.createElement('div');
section.className=`inventory-section inventory-${category.cursor?'cursors':'items'}-section`;section.dataset.category=category.key;section.open=!inventoryCollapsedCategories.has(category.key);
title.className='inventory-section-title';title.append(titleText,countBadge);list.className=category.cursor?'inventory-cursor-grid':'inventory-item-list';section.append(title,list);
section.addEventListener('toggle',()=>{if(section.open)inventoryCollapsedCategories.delete(category.key);else inventoryCollapsedCategories.add(category.key)});
const view={section,title,titleText,countBadge,list,cursor:category.cursor};inventoryCategoryViews.set(category.key,view);perfCount('inventoryNodesCreated',4);return view;
}
function createInventoryItemView(item,cursor){
if(cursor){
const option=document.createElement('button');option.type='button';option.className='inventory-cursor-option';option.dataset.itemId=item.id;option.addEventListener('click',event=>{event.preventDefault();void useInventoryItem(option.dataset.itemId)});
const view={root:option,option,cursor:true,itemId:item.id};inventoryItemViews.set(item.id,view);perfCount('inventoryNodesCreated');return view;
}
const card=document.createElement('article'),icon=document.createElement('div'),copy=document.createElement('div'),name=document.createElement('h3'),description=document.createElement('p'),use=document.createElement('button');
card.className='inventory-item';card.dataset.itemId=item.id;icon.className='inventory-item-icon';copy.append(name,description);use.type='button';use.className='inventory-use';use.addEventListener('click',()=>void useInventoryItem(card.dataset.itemId));card.append(icon,copy,use);
const view={root:card,card,icon,name,description,use,cursor:false,itemId:item.id};inventoryItemViews.set(item.id,view);perfCount('inventoryNodesCreated',6);return view;
}
function inventoryItemViewSignature(item,{count}){
return item.cursorStyle?`cursor:${data.cursorStyle===item.cursorStyle}`:`item:${count}:${storeItemActive(item)}`;
}
function updateInventoryItemView(view,item,state){
const signature=inventoryItemViewSignature(item,state);if(view.signature===signature){perfCount('inventoryItemsUnchanged');return false}
view.signature=signature;const{count}=state;
if(view.cursor){
const option=view.option,selected=data.cursorStyle===item.cursorStyle;setItemIcon(option,item);option.classList.toggle('selected',selected);option.setAttribute('aria-pressed',String(selected));option.setAttribute('aria-label',`${item.name}カーソル`);perfCount('inventoryItemsPatched');return true;
}
const active=storeItemActive(item);view.card.classList.remove('debug-available');setItemIcon(view.icon,item);view.name.textContent=`${item.name} · ×${count}`;view.description.textContent=item.description;
view.use.textContent=item.scoreLens?(active?'オン':'オフ'):(active?'装備中':'装備する');view.use.disabled=false;
const pressed=Boolean(item.scoreLens||item.lineColor||item.lineEffect||item.reactionStyle);view.use.classList.toggle('selected',pressed&&active);if(pressed)view.use.setAttribute('aria-pressed',String(active));else view.use.removeAttribute('aria-pressed');perfCount('inventoryItemsPatched');return true;
}
function patchInventoryItems(itemIds){
if(!inventoryHasRendered||!inventoryModal.classList.contains('show'))return 0;const started=perfStart(),scrollPosition=panelScrollPosition(inventoryPanel),debug=debugAllItemsEnabled();let patched=0;
for(const itemId of new Set(itemIds||[])){if(!itemId)continue;const view=inventoryItemViews.get(itemId),item=storeItem(itemId);if(view&&item&&updateInventoryItemView(view,item,{debug,count:inventoryCount(item.id)}))patched++}
restorePanelScroll(inventoryPanel,scrollPosition);perfGauge('inventoryLastPatchItems',patched);perfEnd('inventoryPatch',started);return patched;
}
function placeInventoryNode(parent,node,anchor){
if(node===anchor)return anchor.nextElementSibling;parent.insertBefore(node,anchor);return anchor;
}
function renderInventoryPanel(){
const started=perfStart(),scrollPosition=panelScrollPosition(inventoryPanel);rememberInventoryCategoryState();
const debug=debugAllItemsEnabled(),total=inventoryCount();
inventoryCountEl.textContent=String(total);
inventoryTotal.textContent=String(total);
inventoryTarget.textContent='カテゴリ名を押すと折り畳めます。ラインカラー、エフェクト、カーソルをここで装備できます。';
if(!total){
let empty=inventoryList.querySelector(':scope > .inventory-item.empty');if(!empty){empty=document.createElement('div');empty.className='inventory-item empty';empty.textContent='所持アイテムはありません。';perfCount('inventoryNodesCreated')}
inventoryList.replaceChildren(empty);for(const view of inventoryItemViews.values())view.root.remove();inventoryItemViews.clear();for(const view of inventoryCategoryViews.values())view.section.remove();inventoryCategoryViews.clear();inventorySelectedCursorItemId=null;restorePanelScroll(inventoryPanel,scrollPosition);perfGauge('inventoryMountedItems',0);perfEnd(inventoryHasRendered?'inventoryPatch':'inventoryRender',started);inventoryHasRendered=true;return;
}
inventoryList.querySelector(':scope > .inventory-item.empty')?.remove();
const available=STORE_ITEMS.filter(item=>inventoryCount(item.id)>0),categories=[
{key:'line-colors',title:'ラインカラー',items:available.filter(item=>item.lineColor),cursor:false},
{key:'reactions',title:'リアクション',items:available.filter(item=>item.reactionStyle),cursor:false},
{key:'tools',title:'ツール',items:available.filter(item=>item.scoreLens),cursor:false},
{key:'cursors',title:'カーソル',items:available.filter(item=>item.cursorStyle),cursor:true}
];
const availableIds=new Set(available.map(item=>item.id)),categoryKeys=new Set(),desiredSections=[];
for(const category of categories){
if(!category.items.length)continue;categoryKeys.add(category.key);
const categoryView=inventoryCategoryViews.get(category.key)||createInventoryCategoryView(category);desiredSections.push(categoryView.section);if(categoryView.titleText.nodeValue!==category.title)categoryView.titleText.nodeValue=category.title;const categoryCount=String(category.items.length);if(categoryView.countBadge.textContent!==categoryCount)categoryView.countBadge.textContent=categoryCount;
let itemAnchor=categoryView.list.firstElementChild;
for(const item of category.items){
let itemView=inventoryItemViews.get(item.id);if(!itemView||itemView.cursor!==category.cursor){itemView?.root?.remove?.();inventoryItemViews.delete(item.id);itemView=createInventoryItemView(item,category.cursor)}else perfCount('inventoryNodesReused');
updateInventoryItemView(itemView,item,{debug,count:inventoryCount(item.id)});itemAnchor=placeInventoryNode(categoryView.list,itemView.root,itemAnchor);
}
}
for(const[id,view]of[...inventoryItemViews])if(!availableIds.has(id)){view.root.remove();inventoryItemViews.delete(id);perfCount('inventoryNodesRemoved')}
for(const[key,view]of[...inventoryCategoryViews])if(!categoryKeys.has(key)){view.section.remove();inventoryCategoryViews.delete(key);perfCount('inventoryNodesRemoved',4)}
let sectionAnchor=inventoryList.firstElementChild;for(const section of desiredSections)sectionAnchor=placeInventoryNode(inventoryList,section,sectionAnchor);
inventorySelectedCursorItemId=available.find(item=>item.cursorStyle&&item.cursorStyle===data.cursorStyle)?.id||null;
restorePanelScroll(inventoryPanel,scrollPosition);perfGauge('inventoryMountedItems',inventoryItemViews.size);perfEnd(inventoryHasRendered?'inventoryPatch':'inventoryRender',started);inventoryHasRendered=true;
}
function updateInventoryUi(){
inventoryCountEl.textContent=debugAllItemsEnabled()?'∞':String(inventoryCount());
if(inventoryModal.classList.contains('show'))renderInventoryPanel();
}
function syncInventoryCursorSelection(){
if(!inventoryList)return;const next=STORE_ITEMS.find(item=>item.cursorStyle&&item.cursorStyle===data.cursorStyle)?.id||null;patchInventoryItems([inventorySelectedCursorItemId,next]);inventorySelectedCursorItemId=next;
}
function activeLineColorItem(){return storeItem(data.lineColorStyle)||storeItem(data.starterLineColor)||LINE_COLOR_ITEMS[0]}
function activeLineColorIndex(){const id=activeLineColorItem()?.id,index=LINE_COLOR_ITEMS.findIndex(item=>item.id===id);return index>=0?index:0}
const AURORA_COLOR_INTERVAL=2000,AURORA_RGB_PALETTE=Object.freeze(['79 235 255','126 255 188','255 223 105','255 112 207','171 126 255','105 160 255','255 139 92']);
let auroraRgbTimer=0,auroraNextTickAt=0,auroraVisiblePathCount=0,auroraRgbValue='',auroraPaletteIndex=-1;
function nextAuroraRgb(){auroraPaletteIndex=(auroraPaletteIndex+1)%AURORA_RGB_PALETTE.length;return AURORA_RGB_PALETTE[auroraPaletteIndex]}
function auroraColorHost(){return world||document.documentElement}
function writeAuroraRgb(){
const next=nextAuroraRgb();if(next===auroraRgbValue)return false;auroraRgbValue=next;auroraColorHost().style.setProperty('--aurora-rgb',next);perfCount('auroraColorWrites');return true;
}
function stopAuroraRgbAnimation(resetDeadline=true){
if(auroraRgbTimer)clearTimeout(auroraRgbTimer);auroraRgbTimer=0;if(resetDeadline)auroraNextTickAt=0;
}
function scheduleAuroraRgbTick(){
if(auroraRgbTimer||document.visibilityState==='hidden'||auroraVisiblePathCount<=0)return false;
const now=perfNow();if(!auroraNextTickAt)auroraNextTickAt=now+AURORA_COLOR_INTERVAL;
auroraRgbTimer=setTimeout(()=>{
auroraRgbTimer=0;const started=perfStart();if(document.visibilityState==='hidden'||auroraVisiblePathCount<=0){stopAuroraRgbAnimation();perfEnd('auroraTick',started);return}
const tickNow=perfNow();writeAuroraRgb();do{auroraNextTickAt+=AURORA_COLOR_INTERVAL}while(auroraNextTickAt<=tickNow);perfEnd('auroraTick',started);scheduleAuroraRgbTick();
},Math.max(0,auroraNextTickAt-now));return true;
}
function startAuroraRgbAnimation(){
if(AURORA_COLOR_INTERVAL<=0||document.visibilityState==='hidden'||auroraVisiblePathCount<=0)return false;
if(!auroraRgbValue)writeAuroraRgb();scheduleAuroraRgbTick();return true;
}
function updateAuroraAnimationState(){
perfGauge('auroraActivePaths',auroraVisiblePathCount);if(document.visibilityState==='hidden'||auroraVisiblePathCount<=0){stopAuroraRgbAnimation();return false}return startAuroraRgbAnimation();
}
function updateBoardAuroraPathCount(board){
if(!board)return 0;const nodes=[...(board.pathStrokeNodes||[]),...(board.connectorNodes||[]),...(board.gateDots||[]),...(board.gateKnobs||[]),...(board.gateMarkers||[])],next=nodes.reduce((count,node)=>count+(node?.classList?.contains('line-effect-aurora')?1:0),0),previous=Number(board.auroraPathCount)||0;
if(next!==previous){board.auroraPathCount=next;auroraVisiblePathCount=Math.max(0,auroraVisiblePathCount+next-previous);updateAuroraAnimationState()}return next;
}
function releaseBoardAuroraPathCount(board){
const previous=Number(board?.auroraPathCount)||0;if(!previous)return;board.auroraPathCount=0;auroraVisiblePathCount=Math.max(0,auroraVisiblePathCount-previous);updateAuroraAnimationState();
}
function syncCosmeticAppearance(){
normalizeEquippedCosmeticsInPlace(data);const activeColor=activeLineColorItem(),color=activeColor?.lineColor||LINE_COLORS[0],lineEffect=activeColor?.aurora?'aurora':'none',reactionStyle=REACTION_STYLE_IDS.has(data.reactionStyle)?data.reactionStyle:'classic';
document.body.style.setProperty('--player-line-color',color);document.body.dataset.lineEffect=lineEffect;document.body.dataset.reactionStyle=reactionStyle;updateAuroraAnimationState();prewarmReactionGlyphs(data.lastReaction||REACTION_EMOJIS[0],reactionStyle);
}
function syncCursorAppearance(style){
const selected=cursorModel.item(style),presentation=cursorModel.presentation(style);
document.body.dataset.cursorStyle=presentation.style;document.body.dataset.cursorEmoji=selected?'on':'off';document.body.dataset.cursorMode=presentation.mode;document.body.style.removeProperty('--active-native-cursor');
customEmojiCursor.classList.toggle('flag-cursor',Boolean(selected?.flagAsset));
if(selected?.flagAsset){const image=document.createElement('img');image.src=selected.flagAsset;image.alt='';image.draggable=false;customEmojiCursor.replaceChildren(image)}else customEmojiCursor.textContent=selected?.cursorEmoji||'';
if(!selected){customEmojiCursor.classList.remove('visible');customEmojiCursor.style.transform=''}
syncPickupHandleDesign();
}
function applyCursorStyle(style){data.cursorStyle=style||'default';syncCursorAppearance(data.cursorStyle);if(realtimePendingCursor){realtimePendingCursor.cursorStyle=data.cursorStyle;realtimeSend({type:'cursor',...realtimePendingCursor})}markGlobalDirty();}
let customCursorX=0,customCursorY=0,customCursorInputAt=0,customCursorInputRevision=0,customCursorCommittedRevision=0,customCursorFrame=0,customCursorLastDraw=0;
function scheduleCustomCursorFrame(){
if(customCursorFrame)return;
const paint=timestamp=>{
if(customCursorLastDraw&&timestamp+INTERACTION_FRAME_TOLERANCE_MS<customCursorLastDraw+DRAG_FRAME_INTERVAL){customCursorFrame=requestAnimationFrame(paint);return}
customCursorFrame=0;customCursorLastDraw=timestamp;
commitCustomCursorFrame({x:customCursorX,y:customCursorY,inputAt:customCursorInputAt,revision:customCursorInputRevision},timestamp);
if(customCursorCommittedRevision!==customCursorInputRevision)scheduleCustomCursorFrame();
};
customCursorFrame=requestAnimationFrame(paint);
}
function cancelCustomCursorFrame(){
if(customCursorFrame)cancelAnimationFrame(customCursorFrame);
customCursorFrame=0;customCursorLastDraw=0;
}
function commitCustomCursorFrame(sample,timestamp){
const customActive=document.body.dataset.cursorMode==='dom'&&!document.body.classList.contains('is-drawing');
if(!customActive){customCursorCommittedRevision=sample.revision;customEmojiCursor.classList.remove('visible');return}
customEmojiCursor.style.transform=`translate3d(${sample.x}px,${sample.y}px,0) translate(-50%,-50%)`;customEmojiCursor.classList.add('visible');
recordInteractionCommit('cursor',timestamp,sample.inputAt,true);customCursorCommittedRevision=sample.revision;markVisualFrame(timestamp);perfCount('cursorCommits');perfCount('cursorStyleMutations');
}
function updateCustomCursorFromPointer(event){
const customActive=document.body.dataset.cursorMode==='dom'&&!document.body.classList.contains('is-drawing'),insideDocument=event.target?.isConnected!==false,finePointer=event.pointerType!=='touch';
if(!customActive||!insideDocument||!finePointer){cancelCustomCursorFrame();customEmojiCursor.classList.remove('visible');return}
customCursorX=event.clientX;customCursorY=event.clientY;customCursorInputAt=Number(event.timeStamp)||perfNow();customCursorInputRevision++;
scheduleCustomCursorFrame();
}
window.addEventListener('pointermove',updateCustomCursorFromPointer,{passive:true,capture:true});
if('onpointerrawupdate'in window)window.addEventListener('pointerrawupdate',updateCustomCursorFromPointer,{passive:true,capture:true});
window.addEventListener('pointerout',event=>{if(!event.relatedTarget)customEmojiCursor.classList.remove('visible')},{passive:true});
function flashConfirmedCell(b,cell){
if(!b?.svg?.isConnected||!Array.isArray(cell))return;
const key=ckey(...cell),previous=b.cellFlashAnimations.get(key);if(previous)try{previous.cancel()}catch(_){}
const node=svgEl('rect',{x:PAD+cell[1]*CELL,y:PAD+cell[0]*CELL,width:CELL,height:CELL,class:'cell-confirm-flash'});b.dragLayer.append(node);
let animation=null,timer=0;const cleanup=()=>{node.remove();if(b.cellFlashAnimations.get(key)===controller)b.cellFlashAnimations.delete(key)};
const controller={cancel(){if(animation)try{animation.cancel()}catch(_){}if(timer)clearTimeout(timer);cleanup()}};b.cellFlashAnimations.set(key,controller);
if(typeof node.animate==='function'){try{animation=node.animate([{opacity:.75},{opacity:0}],{duration:260,easing:'ease-out'});animation.finished.catch(()=>{}).finally(cleanup)}catch(_){animation=null}}
if(!animation)timer=setTimeout(cleanup,280);
}
function playConfirmTone(pathLength){const step=Math.min(7,Math.max(0,(pathLength||1)-1));soundTone(250*Math.pow(2,step/12),.055,{gain:.014,type:'sine',slide:1.03})}
function finalizeExitGates(meta){const p=puzzleOf(meta),st=metaState(meta.id);if(!p||!st.solved)return;meta.sealedSides=[];meta.rev=nextRevision();markMetaDirty(meta.id)}
let sessionCompletionCount=0,timeAttackSuggestionShown=false,timeAttackSuggestionPending=false,timeAttackSuggestionTimer=0;
function timeAttackSuggestionReady(){
if(data.timeAttackSuggestionsDisabled||data.timeAttack||timeAttackSuggestionShown||!timeAttackSuggestionPending)return false;
const dialogOpen=[modal,storeModal,inventoryModal,timeAttackModal,settingsModal].some(root=>root?.classList.contains('show')),
drawing=[...rendered.values()].some(board=>board.drawing?.pointerId!=null||board.drawing?.keyboardActive);
return!dialogOpen&&!drawing;
}
function dismissTimeAttackSuggestion(){
clearTimeout(timeAttackSuggestionTimer);timeAttackSuggestionTimer=0;if(timeAttackSuggestion)timeAttackSuggestion.hidden=true;
}
function tryShowTimeAttackSuggestion(){
clearTimeout(timeAttackSuggestionTimer);timeAttackSuggestionTimer=0;
if(!timeAttackSuggestionPending||timeAttackSuggestionShown)return false;
if(data.timeAttackSuggestionsDisabled||data.timeAttack){timeAttackSuggestionPending=false;return false}
if(!timeAttackSuggestionReady()){timeAttackSuggestionTimer=setTimeout(tryShowTimeAttackSuggestion,250);return false}
timeAttackSuggestionPending=false;timeAttackSuggestionShown=true;timeAttackSuggestion.hidden=false;
timeAttackSuggestionTimer=setTimeout(dismissTimeAttackSuggestion,7000);return true;
}
function scheduleTimeAttackSuggestionAfterCompletion(){
sessionCompletionCount++;if(sessionCompletionCount!==10||timeAttackSuggestionShown)return false;
timeAttackSuggestionPending=true;clearTimeout(timeAttackSuggestionTimer);timeAttackSuggestionTimer=setTimeout(tryShowTimeAttackSuggestion,700);return true;
}
function worldUnitAtClient(clientX,clientY){
const rect=getViewportRect(),localX=(clientX-rect.left-cam.x)/cam.scale,localY=(clientY-rect.top-cam.y)/cam.scale;
return[renderOriginX+localX/UNIT,renderOriginY+localY/UNIT];
}
async function useInventoryItemLoaded(itemId){
const item=storeItem(itemId),debug=debugAllItemsEnabled(),entry=inventoryEntries(itemId)[0];
if(!item||(!debug&&!ownsStoreItem(itemId))){toast('そのアイテムは所持していません。');renderInventoryPanel();return false}
if(item.cursorStyle){
const previousCursor=data.cursorStyle,nextCursor=previousCursor===item.cursorStyle?'default':item.cursorStyle;
applyCursorStyle(nextCursor);syncInventoryCursorSelection();
if(!await save(true)){applyCursorStyle(previousCursor);syncInventoryCursorSelection();toast('切り替えできませんでした。');return false}
toast(nextCursor==='default'?'標準カーソルへ戻しました。':`${item.name}へ切り替えました。`);return true;
}
if(item.lineColor){
const previous=data.lineColorStyle;data.lineColorStyle=item.id;syncCosmeticAppearance();renderAll();markGlobalDirty();
if(!await save(true)){data.lineColorStyle=previous;syncCosmeticAppearance();renderAll();markGlobalDirty();patchInventoryItems([previous,item.id]);toast('ラインカラーを変更できませんでした。');return false}
patchInventoryItems([previous,item.id]);toast(`${item.name}を装備しました。新しく引く線に適用されます。`);return true;
}
if(item.lineEffect){
const previous=data.lineEffectStyle;data.lineEffectStyle=previous===item.lineEffect?'none':item.lineEffect;syncCosmeticAppearance();markGlobalDirty();
const previousItemId=inventoryLineEffectItemIds.get(previous)||null,nextItemId=inventoryLineEffectItemIds.get(data.lineEffectStyle)||null;
if(!await save(true)){data.lineEffectStyle=previous;syncCosmeticAppearance();markGlobalDirty();patchInventoryItems([previousItemId,nextItemId]);toast('ラインエフェクトを変更できませんでした。');return false}
patchInventoryItems([previousItemId,nextItemId]);toast(data.lineEffectStyle==='none'?'ラインエフェクトを外しました。':`${item.name}を装備しました。`);return true;
}
if(item.reactionStyle){
const previous=data.reactionStyle;data.reactionStyle=previous===item.reactionStyle?'classic':item.reactionStyle;syncCosmeticAppearance();markGlobalDirty();
const previousItemId=inventoryReactionStyleItemIds.get(previous)||null,nextItemId=inventoryReactionStyleItemIds.get(data.reactionStyle)||null;
if(!await save(true)){data.reactionStyle=previous;syncCosmeticAppearance();markGlobalDirty();patchInventoryItems([previousItemId,nextItemId]);toast('リアクションを変更できませんでした。');return false}
patchInventoryItems([previousItemId,nextItemId]);toast(data.reactionStyle==='classic'?'標準の絵文字表示に戻しました。':`${item.name}を装備しました。`);return true;
}
if(item.scoreLens){
const previous=data.scoreLensEnabled===true;data.scoreLensEnabled=!previous;markGlobalDirty();invalidateEconomyCaches();
if(!await save(true)){data.scoreLensEnabled=previous;markGlobalDirty();invalidateEconomyCaches();patchInventoryItems([item.id]);toast('切り替えできませんでした。');return false}
updateHud();renderAll();updateZoomPresentation(true);patchInventoryItems([item.id]);toast(`${item.name} ${data.scoreLensEnabled?'オン':'オフ'}`);return true;
}
const previous=entry?deepClone(entry.purchase):null,previousRev=entry?.st?.rev;
if(!await save(true)){
if(entry&&previous){Object.assign(entry.purchase,previous);entry.st.rev=previousRev;markStateDirty(entry.meta.id)}
renderInventoryPanel();toast('使用できませんでした。');return false;
}
updateHud();renderAll();renderInventoryPanel();toast(`${item.name}を装備しました。`);return true;
}
async function useInventoryItem(itemId){
const entry=inventoryEntries(itemId)[0];
if(entry?.meta&&!entry.meta.puzzle)await hydrateMeta(entry.meta);
return useInventoryItemLoaded(itemId);
}
function openInventory(){
if(modal.classList.contains('show'))closeHelp(false);
if(storeModal.classList.contains('show'))closeStore(false);
if(timeAttackModal.classList.contains('show'))closeTimeAttack(false);
if(settingsModal.classList.contains('show'))closeSettings(false);
renderInventoryPanel();
openDialogRoot(inventoryModal);inventoryBtn.setAttribute('aria-expanded','true');
requestAnimationFrame(()=>inventoryPanel.focus());
}
function closeInventory(restoreFocus=true){
closeDialogRoot(inventoryModal,restoreFocus?inventoryBtn:viewport);inventoryBtn.setAttribute('aria-expanded','false');
}
function formatTimeAttackClock(milliseconds){
const seconds=Math.max(0,Math.ceil(milliseconds/1000)),minutes=Math.floor(seconds/60);
return`${String(minutes).padStart(2,'0')}:${String(seconds%60).padStart(2,'0')}`;
}
function waitForTimeAttackCountdown(milliseconds){return new Promise(resolve=>setTimeout(resolve,milliseconds))}
function showTimeAttackCountdownOverlay(value,mode){
clearTimeout(timeAttackCountdownHideTimer);timeAttackCountdownHideTimer=0;
timeAttackCountdownOverlay.hidden=false;timeAttackCountdownOverlay.className='';timeAttackCountdownValue.textContent=String(value);
void timeAttackCountdownOverlay.offsetWidth;
timeAttackCountdownOverlay.classList.add(mode==='final'?'final-sequence':'start-sequence');
if(value==='Start')timeAttackCountdownOverlay.classList.add('is-start');
}
function hideTimeAttackCountdownOverlay(resetFinal=false){
clearTimeout(timeAttackCountdownHideTimer);timeAttackCountdownHideTimer=0;timeAttackCountdownOverlay.hidden=true;timeAttackCountdownOverlay.className='';
if(resetFinal)timeAttackFinalCountdownSecond=null;
}
async function playTimeAttackStartLeadIn(){
timeAttackCountdownActive=true;timeAttackFinalCountdownSecond=null;document.activeElement?.blur?.();
for(const value of['3','2','1']){showTimeAttackCountdownOverlay(value,'start');await waitForTimeAttackCountdown(850)}
}
function releaseTimeAttackStartOverlay(){timeAttackCountdownActive=false;hideTimeAttackCountdownOverlay()}
function pulseTimeAttackFinalCountdown(seconds){
if(timeAttackCountdownActive||seconds===timeAttackFinalCountdownSecond)return;
timeAttackFinalCountdownSecond=seconds;showTimeAttackCountdownOverlay(seconds,'final');
timeAttackCountdownHideTimer=setTimeout(()=>hideTimeAttackCountdownOverlay(),540);
}
function timeAttackResultText(result=data.lastTimeAttack){
if(!result)return'';
return[
'⏱️ LinkFieldリンクフィールドタイムアタック',
`🏁 ${result.durationMinutes}分 🧩 ${result.solves}枚クリア`,
`🏆 合計 ${formatScore(result.total)}`,
cloudAppBaseUrl
].join('\n');
}
function timeAttackCooldownRemaining(_durationMinutes,now=trustedNow()){
const sharedEndsAt=Math.max(0,...TIME_ATTACK_MINUTES.map(minutes=>Number(data.timeAttackCooldowns?.[minutes])||0));
return Math.max(0,sharedEndsAt-now);
}
function hasActiveTimeAttackCooldown(){return timeAttackCooldownRemaining(TIME_ATTACK_MINUTES[0])>0}
function renderTimeAttackPanel(){
const run=data.timeAttack,last=data.lastTimeAttack;
timeAttackSetup.hidden=!!run;
timeAttackLive.hidden=!run;
timeAttackResultEl.hidden=!!run||!last;
copyTimeAttackBtn.hidden=!!run||!last;
closeTimeAttackBtn.textContent='\u9589\u3058\u308b';
for(const button of timeAttackModal.querySelectorAll('[data-time-minutes]')){
const minutes=Number(button.dataset.timeMinutes),remaining=timeAttackCooldownRemaining(minutes),label=button.querySelector('.duration-status'),idleLabel=minutes===3?'短時間で集中':minutes===5?'標準コース':'じっくり挑戦';
button.disabled=remaining>0;
if(label)label.textContent=remaining?`待機 ${formatTimeAttackClock(remaining)}`:idleLabel;
button.title=remaining?`\u518d\u6311\u6226\u307e\u3067\uff1a${formatTimeAttackClock(remaining)}`:`${minutes}\u5206\u306e\u30bf\u30a4\u30e0\u30a2\u30bf\u30c3\u30af\u3092\u958b\u59cb`;
}
if(run){
const remaining=Math.max(0,run.endsAt-trustedNow()),multiplier=timeAttackMultiplier(run.baseCollected||0);
timeAttackClockEl.textContent=formatTimeAttackClock(remaining);
timeAttackClockEl.classList.toggle('urgent',remaining<=30000);
timeAttackCollectedEl.textContent=formatScore(run.collected);
timeAttackMultiplierEl.textContent=`\u00d7${multiplier.toFixed(2)}`;
timeAttackProjectedEl.textContent=formatScore(run.collected);
timeAttackHeadingScore.textContent=`${run.solves}\u679a\u30af\u30ea\u30a2`;
}else{
timeAttackClockEl.classList.remove('urgent');
timeAttackHeadingScore.textContent=last?`\u5408\u8a08 ${formatScore(last.total)}`:'\u6311\u6226\u7d50\u679c';
}
if(last){
timeAttackResultLimit.textContent=`${last.durationMinutes}\u5206`;
timeAttackResultSolves.textContent=String(last.solves);
timeAttackResultTotal.textContent=formatScore(last.total);
timeAttackShareText.textContent=timeAttackResultText(last);
}else timeAttackShareText.textContent='';
}
function updateTimeAttackUi(){
const run=data.timeAttack,now=trustedNow();
if(run&&now>=run.endsAt){hideTimeAttackCountdownOverlay(true);void finishTimeAttack();return}
if(run){
const remaining=Math.max(0,run.endsAt-now),seconds=Math.ceil(remaining/1000);
timeAttackBtn.classList.toggle('final-countdown',remaining<=30000&&remaining>0);
if(!timeAttackCountdownActive){timeAttackFinalCountdownSecond=null;hideTimeAttackCountdownOverlay()}
}else{timeAttackBtn.classList.remove('final-countdown');if(!timeAttackCountdownActive)hideTimeAttackCountdownOverlay(true);}
if(!run&&!hasActiveTimeAttackCooldown())clearTimeAttackTimer()
timeAttackBtn.classList.toggle('active',!!run);
timeAttackBtn.classList.toggle('starting',Boolean(run&&now-run.startedAt<5000));
timeAttackBtn.setAttribute('aria-label',run?`\u6b8b\u308a\u6642\u9593 ${formatTimeAttackClock(run.endsAt-now)}`:'\u30bf\u30a4\u30e0\u30a2\u30bf\u30c3\u30af\u3092\u958b\u304f');
timeAttackButtonLabel.textContent=run?`${formatTimeAttackClock(run.endsAt-now)}`:'\u30bf\u30a4\u30e0\u30a2\u30bf\u30c3\u30af';
if(timeAttackModal.classList.contains('show'))renderTimeAttackPanel();
}
function clearTimeAttackTimer(){if(timeAttackTimer){clearInterval(timeAttackTimer);clearTimeout(timeAttackTimer);timeAttackTimer=null}}
function resumeTimeAttackTimer(){
clearTimeAttackTimer();updateTimeAttackUi();
if(data.timeAttack){timeAttackTimer=setInterval(updateTimeAttackUi,250);return}
const remaining=TIME_ATTACK_MINUTES.map(minutes=>timeAttackCooldownRemaining(minutes)).filter(Boolean);
if(!remaining.length)return;
if(timeAttackModal.classList.contains('show'))timeAttackTimer=setInterval(updateTimeAttackUi,1000);
else timeAttackTimer=setTimeout(resumeTimeAttackTimer,Math.min(...remaining)+50);
}
async function startTimeAttack(durationMinutes){
if(data.timeAttack||timeAttackCountdownActive||!TIME_ATTACK_MINUTES.includes(durationMinutes))return false;
const cooldown=timeAttackCooldownRemaining(durationMinutes);
if(cooldown){toast(`\u518d\u6311\u6226\u307e\u3067\uff1a${formatTimeAttackClock(cooldown)}`);return false}
closeTimeAttack(false);
await playTimeAttackStartLeadIn();
const refreshedCooldown=timeAttackCooldownRemaining(durationMinutes);
if(data.timeAttack||refreshedCooldown){releaseTimeAttackStartOverlay();toast(data.timeAttack?'\u5225\u306e\u30bf\u30a4\u30e0\u30a2\u30bf\u30c3\u30af\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f\u3002':`\u518d\u6311\u6226\u307e\u3067\uff1a${formatTimeAttackClock(refreshedCooldown)}`);return false}
showTimeAttackCountdownOverlay('Start','start');
await waitForTimeAttackCountdown(700);
releaseTimeAttackStartOverlay();
const startedAt=trustedNow(),previous={timeAttack:data.timeAttack,timeAttackRev:data.timeAttackRev};
data.timeAttack={id:globalThis.crypto?.randomUUID?.()||`run-${startedAt}`,durationMinutes,startedAt,endsAt:startedAt+durationMinutes*60000,collected:0,baseCollected:0,rewardPipelineVersion:2,scoreVersion:SCORE_VERSION,solves:0};
data.timeAttackRev=nextRevision();markGlobalDirty();resumeTimeAttackTimer();updateHud();
if(!await save(true)){data.timeAttack=previous.timeAttack;data.timeAttackRev=previous.timeAttackRev;releaseTimeAttackStartOverlay();resumeTimeAttackTimer();toast('\u30bf\u30a4\u30e0\u30a2\u30bf\u30c3\u30af\u3092\u958b\u59cb\u3067\u304d\u307e\u305b\u3093\u3002');return false}
toast(`${durationMinutes}\u5206\u306e\u30bf\u30a4\u30e0\u30a2\u30bf\u30c3\u30af\u3092\u958b\u59cb\u3057\u307e\u3057\u305f\u3002`);
return true;
}
function recordTimeAttackScore(reward){
const run=data.timeAttack;
if(!run)return false;
if(trustedNow()>=run.endsAt){void finishTimeAttack();return false}
const award=Math.max(0,Math.round(typeof reward==='object'?reward.award:reward||0));
const baseAward=Math.max(0,Math.round(typeof reward==='object'?reward.preTimeAward:award));
run.baseCollected=Math.min(MAX_SCORE,(run.baseCollected||0)+baseAward);
run.collected=Math.min(MAX_SCORE,run.collected+award);
run.solves=Math.min(MAX_SOLVES,run.solves+1);
data.timeAttackRev=nextRevision();
updateTimeAttackUi();
return true;
}
let finishingTimeAttack=false;
async function finishTimeAttack(){
if(finishingTimeAttack)return null;
const run=data.timeAttack;if(!run)return null;finishingTimeAttack=true;
const previous={run:deepClone(run),lastTimeAttack:deepClone(data.lastTimeAttack),cooldowns:deepClone(data.timeAttackCooldowns),bonusScore:data.bonusScore,bonusEvents:deepClone(data.bonusEvents),timeAttackRev:data.timeAttackRev};
try{
const result=normalizeTimeAttackResult({...run,completedAt:trustedNow()});data.timeAttack=null;data.lastTimeAttack=result;
data.timeAttackCooldowns=normalizeTimeAttackCooldowns(data.timeAttackCooldowns);const cooldownEndsAt=result.completedAt+TIME_ATTACK_COOLDOWN_MINUTES[result.durationMinutes]*60000;for(const minutes of TIME_ATTACK_MINUTES)data.timeAttackCooldowns[minutes]=cooldownEndsAt;
data.bonusEvents=data.bonusEvents||{};if(result.bonus>0)data.bonusEvents[`time-attack:${result.id}`]=result.bonus;data.bonusScore=bonusEventTotal();statsDirty=true;data.timeAttackRev=nextRevision();markGlobalDirty();clearTimeAttackTimer();
if(!await save(true)){data.timeAttack=previous.run;data.lastTimeAttack=previous.lastTimeAttack;data.timeAttackCooldowns=previous.cooldowns;data.bonusScore=previous.bonusScore;data.bonusEvents=previous.bonusEvents;data.timeAttackRev=previous.timeAttackRev;statsDirty=true;resumeTimeAttackTimer();showStatus('\u4fdd\u5b58\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002',{retry:true,fresh:false});return null}
hideTimeAttackCountdownOverlay(true);updateHud();resumeTimeAttackTimer();openTimeAttack();toast(`\u30bf\u30a4\u30e0\u30a2\u30bf\u30c3\u30af\u7d42\u4e86 \u00b7 \u5408\u8a08 ${formatScore(result.total)}`,3000);return result;
}finally{finishingTimeAttack=false}
}
async function copyTimeAttackResult(){
const text=timeAttackResultText();
if(!text)return false;
let copied=false;
try{await navigator.clipboard.writeText(text);copied=true}
catch(_){
const area=document.createElement('textarea');
area.value=text;area.setAttribute('readonly','');area.style.position='fixed';area.style.opacity='0';
document.body.append(area);area.select();
try{copied=document.execCommand('copy')}catch(__){copied=false}
area.remove();
}
toast(copied?'\u7d50\u679c\u3092\u30b3\u30d4\u30fc\u3057\u307e\u3057\u305f\u3002':'\u7d50\u679c\u3092\u30b3\u30d4\u30fc\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002');
return copied;
}
function openTimeAttack(){
if(modal.classList.contains('show'))closeHelp(false);
if(storeModal.classList.contains('show'))closeStore(false);
if(inventoryModal.classList.contains('show'))closeInventory(false);
if(settingsModal.classList.contains('show'))closeSettings(false);
renderTimeAttackPanel();
openDialogRoot(timeAttackModal);timeAttackBtn.setAttribute('aria-expanded','true');
resumeTimeAttackTimer();requestAnimationFrame(()=>timeAttackPanel.focus());
}
function closeTimeAttack(restoreFocus=true){
closeDialogRoot(timeAttackModal,restoreFocus?timeAttackBtn:viewport);timeAttackBtn.setAttribute('aria-expanded','false');
resumeTimeAttackTimer();
}
function openHelp(entry=false){
if(storeModal.classList.contains('show'))closeStore(false);
if(inventoryModal.classList.contains('show'))closeInventory(false);
if(timeAttackModal.classList.contains('show'))closeTimeAttack(false);
if(settingsModal.classList.contains('show'))closeSettings(false);
entryChoicePending=entry===true;modal.classList.toggle('entry-choice',entryChoicePending);
openDialogRoot(modal);helpBtn.setAttribute('aria-expanded','true');
requestAnimationFrame(()=>helpPanel.focus());
}
function closeHelp(restoreFocus=true,force=false){
if(entryChoicePending&&!force)return false;
closeDialogRoot(modal,restoreFocus?helpBtn:viewport);modal.classList.remove('entry-choice');helpBtn.setAttribute('aria-expanded','false');
return true;
}
function chooseNormalPlay(){entryChoicePending=false;closeHelp(false,true);viewport.focus?.()}
function chooseTimeAttack(){entryChoicePending=false;closeHelp(false,true);openTimeAttack()}
function openSettings(){
if(modal.classList.contains('show'))closeHelp(false,true);
if(storeModal.classList.contains('show'))closeStore(false);
if(inventoryModal.classList.contains('show'))closeInventory(false);
if(timeAttackModal.classList.contains('show'))closeTimeAttack(false);
settingsPlayerName.value=currentPlayerName();lightweightRenderingToggle.checked=uiSettings.lightweightRendering;soundEnabledToggle.checked=uiSettings.soundEnabled;
openDialogRoot(settingsModal);settingsBtn?.setAttribute('aria-expanded','true');requestAnimationFrame(()=>{settingsPanel.focus();settingsPlayerName.select()});
}
function closeSettingsDialog(restoreFocus=true){closeDialogRoot(settingsModal,restoreFocus?(settingsBtn||playerNameBtn):viewport);settingsBtn?.setAttribute('aria-expanded','false')}
function resetSettingsForm(){settingsPlayerName.value=createAutomaticPlayerName();lightweightRenderingToggle.checked=false;soundEnabledToggle.checked=true;toast('設定内容を初期値に戻しました。閉じると反映されます。');settingsPlayerName.focus()}
async function saveSettings(restoreFocus=true){
const name=normalizePlayerNameInput(settingsPlayerName.value);if(!name){toast('プレイヤー名を入力してください。');settingsPlayerName.focus();return false}
const previousName=currentPlayerName(),previousLightweight=uiSettings.lightweightRendering,nameChanged=name!==previousName;
uiSettings=normalizeUiSettings({lightweightRendering:lightweightRenderingToggle.checked,soundEnabled:soundEnabledToggle.checked});persistUiSettings();
if(previousLightweight!==uiSettings.lightweightRendering){for(const board of rendered.values())board.solvedPathsRendered=false;renderAll()}
closeSettingsDialog(restoreFocus);toast('設定を反映しました。');
if(nameChanged)try{await commitPlayerProfileName(name)}catch(error){toast('共有プロフィールを更新できませんでした。')}
return true
}
function closeSettings(restoreFocus=true){return saveSettings(restoreFocus)}
function purchaseFlyOrigin(sourceElement){
const rect=sourceElement?.getBoundingClientRect?.();
return rect&&rect.width>0&&rect.height>0?{left:rect.left,top:rect.top,width:rect.width,height:rect.height}:null;
}
function animatePurchasedItemToInventory(item,origin){
if(!item||!origin||!inventoryBtn?.isConnected)return;
const target=inventoryBtn.getBoundingClientRect(),fly=document.createElement('div');
fly.className='purchase-item-fly';setItemIcon(fly,item);fly.setAttribute('aria-hidden','true');
const startX=origin.left+origin.width/2,startY=origin.top+origin.height/2,endX=target.left+target.width/2,endY=target.top+target.height/2;
fly.style.left=`${startX}px`;fly.style.top=`${startY}px`;document.body.append(fly);
const dx=endX-startX,dy=endY-startY,arc=-Math.max(42,Math.min(150,Math.abs(dx)*.18+Math.abs(dy)*.16));
if(typeof fly.animate!=='function'){fly.style.transform=`translate(calc(-50% + ${dx}px),calc(-50% + ${dy}px)) scale(.28)`;setTimeout(()=>{fly.remove();inventoryBtn.classList.add('purchase-arrival');setTimeout(()=>inventoryBtn.classList.remove('purchase-arrival'),420)},620);return}
const animation=fly.animate([
{transform:'translate(-50%,-50%) scale(1) rotate(0deg)',opacity:1,offset:0},
{transform:`translate(calc(-50% + ${dx*.48}px),calc(-50% + ${dy*.48+arc}px)) scale(1.18) rotate(12deg)`,opacity:1,offset:.52},
{transform:`translate(calc(-50% + ${dx}px),calc(-50% + ${dy}px)) scale(.28) rotate(34deg)`,opacity:.1,offset:1}
],{duration:620,easing:'cubic-bezier(.18,.78,.18,1)',fill:'forwards'});
inventoryBtn.classList.remove('purchase-arrival');
animation.finished.then(()=>{fly.remove();inventoryBtn.classList.add('purchase-arrival');setTimeout(()=>inventoryBtn.classList.remove('purchase-arrival'),420)}).catch(()=>fly.remove());
}
function renderStorePanel(){
const scrollPosition=panelScrollPosition(storePanel);
pruneAndCount();
const meta=data.metas[openStoreBoardId],st=meta?metaState(meta.id):null,store=st?.store;
if(!meta||!store){closeStore(false);return}
storeTitle.textContent='アイテムショップ';
storeWallet.textContent=debugAllItemsEnabled()?'∞':formatScore(data.score);
storeMeta.textContent=`店主:${store.owner}`;
storeInventory.replaceChildren();
const available=storeInventoryItems(meta,store),categories=[
{title:'カーソル',items:available.filter(item=>item.cursorStyle),compact:true,cursor:true,description:'盤面操作に使うカーソルデザインです。'},
{title:'その他のアイテム',items:available.filter(item=>!item.cursorStyle).slice(0,6),compact:false,cursor:false,description:''}
];
for(const category of categories){
if(!category.items.length)continue;
const section=document.createElement('section'),title=document.createElement('h3'),brief=document.createElement('p'),list=document.createElement('div');
section.className=`store-section store-${category.cursor?'cursors':'items'}-section`;title.className='store-section-title';title.textContent=category.title;brief.className='store-section-brief';brief.textContent=category.description;
list.className=`store-section-list ${category.compact?'store-compact-list':'store-item-list'}${category.cursor?' store-cursor-list':''}`;
for(const item of category.items){
const purchased=personalEconomyMode()?Boolean(playerPurchaseForStore(meta.id,item.id)):store.purchases.some(purchase=>purchase.id===item.id),price=storeItemPrice(meta,store,item),
card=document.createElement('article'),icon=document.createElement('div'),buy=document.createElement('button');
card.className=`store-item${category.cursor?' store-cursor':' store-other'}${category.compact?' store-compact':''}${purchased?' purchased':''}`;
card.title=`${item.name}${item.effectLabel}${item.description}`;icon.className='store-item-icon';setItemIcon(icon,item);
buy.type='button';buy.className='store-buy';
buy.textContent=purchased?'購入済み':`購入 ${formatScore(price)}`;
buy.disabled=purchased||(!debugAllItemsEnabled()&&data.score<price)||(!debugAllItemsEnabled()&&personalEconomyMode()&&!onlinePlayerEconomy());buy.setAttribute('aria-label',`${item.name}${buy.textContent}`);
buy.addEventListener('click',()=>purchaseStoreItem(meta.id,item.id,purchaseFlyOrigin(icon)));
if(category.compact)card.append(icon,buy);
else{
const copy=document.createElement('div'),name=document.createElement('h4'),effect=document.createElement('strong'),description=document.createElement('p');
copy.className='store-item-copy';name.textContent=item.name;effect.textContent=item.effectLabel;description.textContent=item.description;
copy.append(name,effect,description);card.append(icon,copy,buy);
}
list.append(card);
}
section.append(title);if(category.description)section.append(brief);section.append(list);storeInventory.append(section);
}
restorePanelScroll(storePanel,scrollPosition);
}
function openStoreMeta(meta){
if(meta&&!meta.puzzle){void hydrateMeta(meta).then(()=>openStoreMeta(meta)).catch(error=>{console.warn('BEND FIELD: shop hydration failed',error);toast('\u30b7\u30e7\u30c3\u30d7\u3092\u8aad\u307f\u8fbc\u3081\u307e\u305b\u3093\u3067\u3057\u305f\u3002')});return true}
const store=meta&&metaState(meta.id).store;if(!store)return false;
if(modal.classList.contains('show'))closeHelp(false);
if(inventoryModal.classList.contains('show'))closeInventory(false);
if(timeAttackModal.classList.contains('show'))closeTimeAttack(false);
if(settingsModal.classList.contains('show'))closeSettings(false);
openStoreBoardId=meta.id;renderStorePanel();openDialogRoot(storeModal);requestAnimationFrame(()=>storePanel.focus());return true;
}
function openStore(board){return openStoreMeta(board?.meta)}
function closeStore(restoreFocus=true){
const board=rendered.get(openStoreBoardId);
closeDialogRoot(storeModal,restoreFocus&&board?.storeButton?.isConnected?board.storeButton:viewport);openStoreBoardId=null;
}
async function purchaseStoreItem(boardId,itemId,purchaseOrigin=null){
const item=storeItem(itemId);if(!item)return false;
pruneAndCount();
const meta=data.metas[boardId],st=meta?metaState(boardId):null,store=st?.store;if(!store)return false;
if(!storeInventoryItems(meta,store).some(available=>available.id===item.id)){toast('この店では取り扱っていません。');return false}
const debug=debugAllItemsEnabled();if(!debug&&personalEconomyMode()&&!onlinePlayerEconomy()){toast('共有の所持数を確認できないため購入できません。');renderStorePanel();return false}
const already=personalEconomyMode()?playerPurchaseForStore(boardId,itemId):store.purchases.find(purchase=>purchase.id===item.id);if(already){toast('購入済みです。');return false}
const price=storeItemPrice(meta,store,item);if(!debug&&data.score<price){toast(`所持数が不足しています。価格 ${formatScore(price)}`);renderStorePanel();return false}
if(debug&&personalEconomyMode()){const purchase={purchaseId:`debug-${sessionId}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`,boardId,itemId:item.id,buyer:currentPlayerName(),boughtAt:trustedNow(),paidCost:0};data.playerPurchases=[...normalizePlayerPurchases(data.playerPurchases),purchase];invalidateEconomyCaches();markGlobalDirty(false);if(!await save(true)){data.playerPurchases=data.playerPurchases.filter(entry=>entry.purchaseId!==purchase.purchaseId);invalidateEconomyCaches();toast('購入できませんでした。');return false}updateHud();renderStorePanel();animatePurchasedItemToInventory(item,purchaseOrigin);playSound('buy');toast(`${item.name}を購入しました。`);return true}
if(onlinePlayerEconomy())try{await buyPersonalStoreItem(boardId,itemId);updateHud();renderStorePanel();animatePurchasedItemToInventory(item,purchaseOrigin);playSound('buy');toast(`${item.name}を購入しました。`);return true}catch(error){toast('購入できませんでした。共有状態を確認してください。');renderStorePanel();return false}
const previousRev=st.rev,purchase={id:item.id,buyer:currentPlayerName(),boughtAt:trustedNow(),paidCost:debug?0:price};
store.purchases.push(purchase);st.rev=nextRevision();markStateDirty(boardId);
if(!await save(true)){store.purchases.splice(store.purchases.indexOf(purchase),1);st.rev=previousRev;markStateDirty(boardId);updateHud();renderStorePanel();toast('購入できませんでした。');return false}
updateHud();renderStorePanel();animatePurchasedItemToInventory(item,purchaseOrigin);playSound('buy');toast(`${item.name}を購入しました。`);return true;
}
window.addEventListener('keydown',e=>{
if(settingsModal.classList.contains('show')){
if(e.key==='Escape'){e.preventDefault();void closeSettings();return}
if(e.key==='Enter'&&document.activeElement===settingsPlayerName){e.preventDefault();void closeSettings();return}
trapDialogFocus(settingsPanel,e);return;
}
if(inventoryModal.classList.contains('show')){
if(e.key==='Escape'){e.preventDefault();closeInventory();return}
trapDialogFocus(inventoryPanel,e);return;
}
if(timeAttackModal.classList.contains('show')){
if(e.key==='Escape'){e.preventDefault();closeTimeAttack();return}
trapDialogFocus(timeAttackPanel,e);return;
}
if(storeModal.classList.contains('show')){
if(e.key==='Escape'){e.preventDefault();closeStore();return}
trapDialogFocus(storePanel,e);return;
}
if(modal.classList.contains('show')){
if(e.key==='Escape'){e.preventDefault();closeHelp();return}
trapDialogFocus(helpPanel,e);return;
}
if(e.key!=='Escape')return;
const b=rendered.get(activeBoard);
if(b?.drawing){e.preventDefault();b.drawing=null;changed(b.id);renderBoard(b);updateSelectedProgress(b);queueMicrotask(reconcileDrawingPresentation);toast('\u7dda\u306e\u7de8\u96c6\u3092\u7d42\u4e86\u3057\u307e\u3057\u305f\u3002')}
});
helpBtn.onclick=openHelp;
settingsBtn.onclick=openSettings;
playerNameBtn.onclick=openSettings;
resetSettingsBtn.onclick=resetSettingsForm;
closeSettingsBtn.onclick=()=>{void closeSettings()};
settingsModal.addEventListener('pointerdown',e=>{if(e.target===settingsModal)void closeSettings()});
inventoryBtn.onclick=openInventory;
document.querySelector('#closeInventory').onclick=()=>closeInventory();
inventoryModal.addEventListener('pointerdown',e=>{if(e.target===inventoryModal)closeInventory()});
document.querySelector('#acceptTimeAttackSuggestion').onclick=()=>{dismissTimeAttackSuggestion();openTimeAttack()};
document.querySelector('#dismissTimeAttackSuggestion').onclick=dismissTimeAttackSuggestion;
timeAttackBtn.onclick=openTimeAttack;
for(const button of timeAttackModal.querySelectorAll('[data-time-minutes]'))button.addEventListener('click',()=>startTimeAttack(Number(button.dataset.timeMinutes)));
copyTimeAttackBtn.onclick=copyTimeAttackResult;
closeTimeAttackBtn.onclick=()=>closeTimeAttack();
timeAttackModal.addEventListener('pointerdown',e=>{if(e.target===timeAttackModal)closeTimeAttack()});
document.querySelector('#normalPlayBtn').onclick=chooseNormalPlay;
document.querySelector('#helpTimeAttackBtn').onclick=chooseTimeAttack;
modal.addEventListener('pointerdown',e=>{if(e.target===modal&&!entryChoicePending)closeHelp()});
document.querySelector('#closeStore').onclick=()=>closeStore();
storeModal.addEventListener('pointerdown',e=>{if(e.target===storeModal)closeStore()});
document.querySelector('#minimapOriginBtn').onclick=()=>centerOrigin();
document.querySelector('#minimapRandomBtn').onclick=()=>{void centerRandomBoard()};
function resetSelectedBoard(selected=rendered.get(hudBoardId)){
const b=selected;if(!b)return false;const st=metaState(b.id);
const hasSpecialProgress=!!st.specialProgress?.crossings?.length;
if(st.solved||!st.paths.length&&!hasSpecialProgress){toast(st.solved?'\u30af\u30ea\u30a2\u6e08\u307f':'\u7dda\u304c\u3042\u308a\u307e\u305b\u3093\u3002');return false}
const performReset=()=>{if(typeof touchBoardClaim==='function')touchBoardClaim(b.id,true);const pointerId=b.drawing?.pointerId;cancelBoardDragFrame(b);if(pointerId!=null)try{if(b.svg.hasPointerCapture?.(pointerId))b.svg.releasePointerCapture(pointerId)}catch(_){}
applyBoardCommand(b,state=>{b.drawing=null;b.armedGate=null;b.solvedPathsRendered=false;state.paths=[];state.specialProgress={crossings:[]}},{persist:true,paint:false});
renderBoardNow(b);updateSelectedProgress(b);queueMicrotask(reconcileDrawingPresentation);toast('\u76e4\u9762\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u307e\u3057\u305f\u3002');return true};
if(typeof cloudApiEnabled==='undefined'||typeof cloudAvailable==='undefined'||!cloudApiEnabled||!cloudAvailable)return performReset();
return ensureBoardClaimForInput(b).then(ok=>ok?performReset():false);
}
world.addEventListener('click',event=>{
const control=event.target.closest?.('.board-reset,.line-store');if(!control)return;
const card=control.closest('.board-card'),board=card?rendered.get(card.dataset.id):null;if(!board)return;
event.preventDefault();event.stopPropagation();selectBoard(board);
if(control.classList.contains('board-reset')){playSound('reset');void resetSelectedBoard(board)}
else{playSound('shop');openStore(board)}
});
saveStatusEl.onclick=async()=>{if(await flushSave())toast('\u4fdd\u5b58\u3057\u307e\u3057\u305f')};
let activeArchiveController=null,fieldArchiveBusy=false;
const dismissStatusBtn=document.querySelector('#dismissStatusBtn'),exportBtn=document.querySelector('#exportBtn');
function setFieldArchiveBusy(busy,message=''){
fieldArchiveBusy=busy;document.body.classList.toggle('archive-busy',busy);
for(const button of[exportBtn,document.querySelector('#importBtn'),document.querySelector('#freshBtn')])if(button)button.disabled=busy;
if(dismissStatusBtn)dismissStatusBtn.textContent=busy?'\u30ad\u30e3\u30f3\u30bb\u30eb':'\u9589\u3058\u308b';
if(message)showStatus(message,{retry:false,fresh:false});
}
dismissStatusBtn.onclick=()=>{if(activeArchiveController)activeArchiveController.abort();else hideStatus()};
document.addEventListener('keydown',event=>{if(!fieldArchiveBusy)return;if(event.key==='Escape'&&activeArchiveController)activeArchiveController.abort();if(event.key!=='Tab'){event.preventDefault();event.stopImmediatePropagation()}},true);
function parseJsonOrRaw(text){if(!text)return null;try{return JSON.parse(text)}catch(_){return{raw:text,parseError:true}}}
function portableGlobalForArchive(source=data){
return{
gameplayVersion:GAMEPLAY_DATA_VERSION,
bonusEvents:deepClone(source.bonusEvents||{}),
specialMechanicsSeen:normalizeSpecialMechanics(source.specialMechanicsSeen),
clockFloor:Math.max(0,Number(source.clockFloor)||0),
lastSolveAt:Math.max(0,Number(source.lastSolveAt)||0),
timeAttack:deepClone(source.timeAttack),
timeAttackCooldowns:deepClone(source.timeAttackCooldowns),
lastTimeAttack:deepClone(source.lastTimeAttack),
timeAttackSuggestionsDisabled:source.timeAttackSuggestionsDisabled===true,
cursorStyle:typeof source.cursorStyle==='string'?source.cursorStyle.slice(0,32):'default',
scoreLensEnabled:source.scoreLensEnabled===true
};
}
function portableMetaForArchive(meta){
const value=metaForStorage(meta);delete value.rev;delete value.revAuthor;return value;
}
function portableStateForArchive(state){
const value=stateForStorage(state);delete value.rev;delete value.revAuthor;return value;
}
async function loadV2ArchiveBoardPage(epoch,afterNumber=-1,limit=32){
const db=await openWorldDb(),tx=db.transaction('boardIndex','readonly'),done=transactionDone(tx),
range=IDBKeyRange.bound([epoch,Math.max(0,afterNumber+1)],[epoch,Number.MAX_SAFE_INTEGER]),
indexes=await requestValue(tx.objectStore('boardIndex').index('epochNumber').getAll(range,limit));await done;
if(!indexes.length)return[];
const detailsTx=db.transaction(['boardPuzzles','boardStates'],'readonly'),detailsDone=transactionDone(detailsTx),
puzzleStore=detailsTx.objectStore('boardPuzzles'),stateStore=detailsTx.objectStore('boardStates'),
[puzzles,states]=await Promise.all([Promise.all(indexes.map(index=>requestValue(puzzleStore.get([epoch,index.id])))),Promise.all(indexes.map(index=>requestValue(stateStore.get([epoch,index.id]))))]);await detailsDone;
return indexes.map((index,position)=>{
const puzzle=puzzles[position],state=states[position];if(!puzzle||!state||puzzle.metaRev!==index.metaRev||state.stateRev!==index.stateRev)throw new Error(`Stored board revisions do not match for ${index.id}.`);
const meta={id:index.id,x:index.x,y:index.y,chunks:index.chunks,level:index.level,targetLevel:index.targetLevel,seed:index.seed,axis:index.axis,entrySide:index.entrySide||null,sealedSides:[...(index.sealedSides||[])],puzzle:puzzle.puzzle,generatorVersion:puzzle.generatorVersion||GENERATOR_VERSION,rev:puzzle.metaRev,revAuthor:puzzle.revAuthor||index.metaRevAuthor||''};
return{id:index.id,number:index.number,meta:portableMetaForArchive(meta),state:portableStateForArchive(state.value)};
});
}
async function* portableBoardsForExport(epoch){
if(activeStorageFormat===FIELD_STORAGE_FORMAT){
let after=-1;
while(true){const page=await loadV2ArchiveBoardPage(epoch,after);if(!page.length)break;for(const board of page){after=board.number;yield{id:board.id,meta:board.meta,state:board.state}}}
return;
}
for(const id of Object.keys(data.metas).sort((a,b)=>(Number(a.slice(1))||0)-(Number(b.slice(1))||0)))yield{id,meta:portableMetaForArchive(data.metas[id]),state:portableStateForArchive(data.states[id]||normalizeState(null))};
}
async function activeWorldArchiveHeader(epoch){
const db=await openWorldDb(),tx=db.transaction(['control','worlds'],'readonly'),done=transactionDone(tx),controlStore=tx.objectStore('control'),worldsStore=tx.objectStore('worlds'),[control,world]=await Promise.all([requestValue(controlStore.get('active')),requestValue(worldsStore.get(epoch))]);await done;
if(!control||control.activeFormat!==FIELD_STORAGE_FORMAT||control.activeEpoch!==epoch||!world||world.status!=='active')throw staleWorldEpochError();return world;
}
async function downloadSaveExport(destination){
const controller=new AbortController();activeArchiveController=controller;setFieldArchiveBusy(true,'\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u66f8\u304d\u51fa\u3057\u3066\u3044\u307e\u3059\u3002');
try{
if(!fieldIndexComplete)await completeV2IndexScan();
if(!await flushSave())throw new Error('The field could not be checkpointed before export.');
return await withWorldMutationLock(async()=>{
if(hasPendingPersistence())await persistDirtyToDb({skipCloud:true});
const epoch=data.worldEpoch,world=await activeWorldArchiveHeader(epoch),boardCount=world.boardCount||Object.keys(data.metas).length,portableGlobal=portableGlobalForArchive(),compression=typeof CompressionStream==='function'?'gzip':'identity',estimatedRawBytes=Math.max(boardCount*1024,(world.approximateBytes||0)+JSON.stringify(portableGlobal).length+boardCount*256),
manifest={saveSchema:SAVE_SCHEMA,gameplayVersion:GAMEPLAY_DATA_VERSION,worldGeneration:WORLD_GENERATION,appVersion:APP_VERSION,generatorVersion:GENERATOR_VERSION,exportedAt:new Date().toISOString(),boardCount,estimatedRawBytes,encoding:'ndjson',compression},
result=await FieldPersistence.writeArchive({writable:destination.writable,manifest,globalState:portableGlobal,boards:portableBoardsForExport(epoch),signal:controller.signal,onProgress:progress=>setSaveStatus('saving',`${progress.boardsDone}/${progress.boardsTotal}`)});
await destination.finish();setSaveStatus('saved','\u66f8\u304d\u51fa\u3057\u5b8c\u4e86');toast('\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u66f8\u304d\u51fa\u3057\u307e\u3057\u305f\u3002');return result;
});
}catch(error){
await destination.abort(error);if(error?.name==='AbortError')toast('\u66f8\u304d\u51fa\u3057\u3092\u30ad\u30e3\u30f3\u30bb\u30eb\u3057\u307e\u3057\u305f\u3002');else throw error;
}finally{activeArchiveController=null;setFieldArchiveBusy(false);hideStatus()}
}
if(exportBtn)exportBtn.onclick=async()=>{
const filename=`bend-field-save-${new Date().toISOString().slice(0,10)}.bfsave`;
try{
const destination=await FieldPersistence.createArchiveDestination(filename,Math.max(1024,Object.keys(data.metas).length*1024));
await downloadSaveExport(destination);
}catch(error){if(error?.name!=='AbortError'){showStatus(`\u66f8\u304d\u51fa\u3057\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 ${error?.message||error}`,{retry:false,fresh:false});toast('\u66f8\u304d\u51fa\u3057\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002')}}
};
async function beginWorldReplacement(reason){
if(!await flushSave())throw new Error('The current field could not be checkpointed.');
lifecyclePersistenceSuppressed=true;clearTimeout(saveTimer);saveTimer=null;cancelMirrorCheckpoint(true);await persistQueue;
if(activeStorageFormat!==FIELD_STORAGE_FORMAT)await preserveRecoveryDurably(JSON.stringify(compactSnapshot()),reason);
return createWorldEpoch();
}
const freshStartButton=document.querySelector('#freshBtn');if(freshStartButton)freshStartButton.onclick=async()=>{
if(!confirm('\u73fe\u5728\u306e\u9032\u884c\u3092\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3057\u3001\u6700\u521d\u304b\u3089\u59cb\u3081\u307e\u3059\u304b\uff1f'))return;
try{const newEpoch=await beginWorldReplacement('User started fresh');await clearDatabaseWorld(newEpoch);clearCompactMirror();clearRecoveryJournalKeys();safeLocalRemove(storageRevisionKey);announceWorldReplacement(newEpoch);location.reload()}
catch(error){lifecyclePersistenceSuppressed=false;showStatus(`\u65b0\u3057\u3044\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u958b\u59cb\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 ${error?.message||error}`,{retry:true,fresh:true})}
};
function validateImportedStateStrict(meta,raw){
if(!isPlainObject(raw)||!Array.isArray(raw.paths)||raw.paths.length>MAX_PATHS_PER_BOARD)throw new Error(`Invalid board state: ${meta.id}`);
const state=normalizeState(raw);if(state.paths.length!==raw.paths.length)throw new Error(`Invalid path record: ${meta.id}`);
const puzzle=meta.puzzle,validCells=cellSet(puzzle),crossingSet=new Set(crossingKeys(puzzle)),usedCells=new Map(),usedGates=new Set();
for(const path of state.paths){
let valid=path.startGate>=0&&path.startGate<puzzle.g.length&&path.endGate!==path.startGate&&(!path.detachedStart||path.endGate==null);
if(valid&&path.endGate!=null)valid=path.endGate>=0&&path.endGate<puzzle.g.length;
if(valid){const start=puzzle.g[path.startGate];valid=path.detachedStart||sameCell(path.cells[0],start);if(path.endGate!=null)valid=valid&&sameCell(path.cells[path.cells.length-1],puzzle.g[path.endGate])}
const ownCells=new Set();
if(valid)for(let index=0;index<path.cells.length;index++){
const cell=path.cells[index],key=ckey(...cell),owners=usedCells.get(key)||0;
if(!validCells.has(key)||ownCells.has(key)||(owners>0&&!crossingSet.has(key))||owners>=2||(index>0&&!pathCellsAdjacent(puzzle,path.cells[index-1],cell))){valid=false;break}
ownCells.add(key);
}
if(valid&&(!path.detachedStart&&usedGates.has(path.startGate)||path.endGate!=null&&usedGates.has(path.endGate)||path.openGate!=null&&usedGates.has(path.openGate)))valid=false;
if(!valid)throw new Error(`Invalid path geometry: ${meta.id}`);
for(const key of ownCells)usedCells.set(key,(usedCells.get(key)||0)+1);
if(!path.detachedStart)usedGates.add(path.startGate);if(path.endGate!=null)usedGates.add(path.endGate);if(path.openGate!=null)usedGates.add(path.openGate);
}
const solved=isSolved(state,puzzle);if(state.solved!==solved)throw new Error(`Solved state does not match the puzzle: ${meta.id}`);
if(!solved&&(state.solvedBy||state.scoreAwarded||state.store))throw new Error(`Unsolved board contains solved-only data: ${meta.id}`);
return state;
}
async function deleteV2Epoch(epoch){
if(!idbAvailable||!validWorldEpoch(epoch))return false;
const db=await openWorldDb(),stores=['boardIndex','boardPuzzles','boardStates','tombstonesV2','recoveryV2','outboxV2'],tx=db.transaction(['worlds',...stores],'readwrite'),done=transactionDone(tx),range=epochKeyRange(epoch);
tx.objectStore('worlds').delete(epoch);for(const name of stores)tx.objectStore(name).delete(range);await done;return true;
}
async function verifyActiveWorldActivation(){
if(!idbAvailable||activeStorageFormat!==FIELD_STORAGE_FORMAT)return false;
const db=await openWorldDb(),control=await readActiveWorldControl(db);
if(!control||control.activationVerified!==false||control.activeEpoch!==data.worldEpoch)return false;
await validateActiveWorldReadiness(control.activeEpoch);
const tx=db.transaction(['control','worlds'],'readwrite'),done=transactionDone(tx),controlStore=tx.objectStore('control'),worldsStore=tx.objectStore('worlds'),
[fresh,active,previous]=await Promise.all([requestValue(controlStore.get('active')),requestValue(worldsStore.get(control.activeEpoch)),control.previousEpoch?requestValue(worldsStore.get(control.previousEpoch)):Promise.resolve(null)]);
if(!fresh||fresh.activeEpoch!==control.activeEpoch||fresh.activationVerified!==false||!active){try{tx.abort()}catch(_){}await done.catch(()=>{});throw staleWorldEpochError()}
if(previous){previous.status='garbage';previous.progress={phase:'cleanup',store:'boardIndex',rows:0};worldsStore.put(previous)}
active.status='active';worldsStore.put(active);
const verified={...fresh,previousEpoch:undefined,activationVerified:true};controlStore.put(verified);await done;startupWorldControl=verified;return true;
}
function stableJson(value){if(Array.isArray(value))return`[${value.map(stableJson).join(',')}]`;if(value&&typeof value==='object')return`{${Object.keys(value).sort().map(key=>`${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;return JSON.stringify(value)}
function boardIndexMatchesDetails(index,meta,state,epoch){
const expected=summarizeBoardV2(meta,state,epoch),keys=['epoch','id','number','x','y','chunks','level','targetLevel','seed','axis','entrySide','metaRev','stateRev','revAuthor','solved','expanded','scoreAwarded','hasProgress','specialFlags','shop'];
return keys.every(key=>stableJson(index?.[key])===stableJson(expected[key]));
}
async function validateStagedWorldV2(epoch,{expectedBoardCount=null,strict=true,allowStaging=false}={}){
if(!validWorldEpoch(epoch))throw new Error('The staged field epoch is invalid.');
const db=await openWorldDb();let after=-1,boardCount=0,solvedCount=0,boardScore=0,maxNumber=0,approximateBytes=0,bounds=null,originOwnedByB0=false;const occupied=new Set();
while(true){
const tx=db.transaction(['boardIndex','boardPuzzles','boardStates'],'readonly'),done=transactionDone(tx),indexStore=tx.objectStore('boardIndex'),puzzleStore=tx.objectStore('boardPuzzles'),stateStore=tx.objectStore('boardStates'),
indexes=await requestValue(indexStore.index('epochNumber').getAll(IDBKeyRange.bound([epoch,Math.max(0,after+1)],[epoch,Number.MAX_SAFE_INTEGER]),512));
if(!indexes.length){await done;break}
const [puzzles,states]=await Promise.all([Promise.all(indexes.map(index=>requestValue(puzzleStore.get([epoch,index.id])))),Promise.all(indexes.map(index=>requestValue(stateStore.get([epoch,index.id]))))]);await done;
for(let position=0;position<indexes.length;position++){
const index=indexes[position],puzzle=puzzles[position],stateRow=states[position],number=Number(index.id?.slice(1));
if(!Number.isSafeInteger(number)||number<0||number<=after||index.number!==number)throw new Error(`The staged board order is invalid: ${index.id}`);
if(!puzzle||!stateRow||puzzle.metaRev!==index.metaRev||stateRow.stateRev!==index.stateRev)throw new Error(`The staged board revisions do not match: ${index.id}`);
const meta=normalizeMeta(index.id,{id:index.id,x:index.x,y:index.y,chunks:index.chunks,level:index.level,targetLevel:index.targetLevel,seed:index.seed,axis:index.axis,entrySide:index.entrySide||null,sealedSides:index.sealedSides||[],puzzle:puzzle.puzzle,generatorVersion:puzzle.generatorVersion||GENERATOR_VERSION,rev:puzzle.metaRev,revAuthor:puzzle.revAuthor||index.metaRevAuthor||''});
if(!meta)throw new Error(`The staged puzzle is invalid: ${index.id}`);
const state=strict?validateImportedStateStrict(meta,stateRow.value):normalizeState(stateRow.value);
if(state.rev!==index.stateRev||!boardIndexMatchesDetails(index,meta,state,epoch))throw new Error(`The staged summary does not match its board details: ${index.id}`);
for(const[dx,dy]of meta.chunks){const x=meta.x+dx,y=meta.y+dy,key=key2(x,y);if(occupied.has(key))throw new Error(`The staged field contains overlapping boards: ${index.id}`);occupied.add(key);if(index.id==='B0'&&x===0&&y===0)originOwnedByB0=true;bounds=bounds?{minX:Math.min(bounds.minX,x),minY:Math.min(bounds.minY,y),maxX:Math.max(bounds.maxX,x+1),maxY:Math.max(bounds.maxY,y+1)}:{minX:x,minY:y,maxX:x+1,maxY:y+1}}
if(boardCount===0&&index.id!=='B0'||index.id==='B0'&&boardCount!==0)throw new Error('B0 is not the first staged board.');
boardCount++;after=number;maxNumber=Math.max(maxNumber,number);if(state.solved){solvedCount++;boardScore=Math.min(MAX_SCORE,boardScore+state.scoreAwarded)}
approximateBytes+=JSON.stringify(index).length+JSON.stringify(puzzle).length+JSON.stringify(stateRow).length;if(position>0&&position%32===0)await new Promise(resolve=>setTimeout(resolve,0));
}
await new Promise(resolve=>setTimeout(resolve,0));
}
if(boardCount<1||!occupied.size||!bounds)throw new Error('The staged field is empty.');
if(!originOwnedByB0||after<0)throw new Error('B0 does not own the staged field origin.');
const countTx=db.transaction(['worlds','boardIndex','boardPuzzles','boardStates'],'readonly'),countDone=transactionDone(countTx),worldsStore=countTx.objectStore('worlds'),
[world,indexCount,puzzleCount,stateCount]=await Promise.all([requestValue(worldsStore.get(epoch)),requestValue(countTx.objectStore('boardIndex').index('epoch').count(IDBKeyRange.only(epoch))),requestValue(countTx.objectStore('boardPuzzles').index('epoch').count(IDBKeyRange.only(epoch))),requestValue(countTx.objectStore('boardStates').index('epoch').count(IDBKeyRange.only(epoch)))]);await countDone;
const allowedStatus=allowStaging?world?.status==='staging'||world?.status==='ready':world?.status==='ready';
if(!world||!allowedStatus||indexCount!==boardCount||puzzleCount!==boardCount||stateCount!==boardCount)throw new Error('The staged field row counts do not match.');
if(expectedBoardCount!=null&&boardCount!==expectedBoardCount)throw new Error('The staged field board count changed during validation.');
const expectedScore=Math.min(MAX_SCORE,boardScore+bonusEventTotal(world.global?.bonusEvents||{}));
if((world.global?.nextId||0)!==maxNumber+1||(world.global?.solved||0)!==solvedCount||(world.global?.score||0)!==expectedScore)throw new Error('The staged global aggregates do not match the board rows.');
return{boardCount,solvedCount,score:expectedScore,boardScore,nextId:maxNumber+1,bounds,approximateBytes};
}
async function validateActiveWorldReadiness(epoch){
const db=await openWorldDb(),tx=db.transaction(['control','worlds','boardIndex','boardPuzzles','boardStates'],'readonly'),done=transactionDone(tx),controlStore=tx.objectStore('control'),worldsStore=tx.objectStore('worlds'),indexStore=tx.objectStore('boardIndex'),
[control,world,indexes,b0Index,b0Puzzle,b0State]=await Promise.all([requestValue(controlStore.get('active')),requestValue(worldsStore.get(epoch)),requestValue(indexStore.index('epochNumber').getAll(IDBKeyRange.bound([epoch,0],[epoch,Number.MAX_SAFE_INTEGER]),512)),requestValue(indexStore.get([epoch,'B0'])),requestValue(tx.objectStore('boardPuzzles').get([epoch,'B0'])),requestValue(tx.objectStore('boardStates').get([epoch,'B0']))]);await done;
if(!control||control.activeEpoch!==epoch||control.activeFormat!==FIELD_STORAGE_FORMAT||!world||world.status!=='active'||!indexes.length||indexes[0].id!=='B0')throw new Error('The activated field header or first index page is invalid.');
const occupied=new Set();for(const index of indexes){if(!/^B(?:0|[1-9]\d*)$/.test(index.id)||!validChunkShape(index.chunks))throw new Error(`The activated field index is invalid: ${index.id}`);for(const[dx,dy]of index.chunks){const key=key2(index.x+dx,index.y+dy);if(occupied.has(key))throw new Error('The activated field first index page overlaps.');occupied.add(key)}}
if(!b0Index||!b0Puzzle||!b0State||b0Index.metaRev!==b0Puzzle.metaRev||b0Index.stateRev!==b0State.stateRev)throw new Error('The activated field failed its B0 revision check.');
const meta=normalizeMeta('B0',{id:'B0',x:b0Index.x,y:b0Index.y,chunks:b0Index.chunks,level:b0Index.level,targetLevel:b0Index.targetLevel,seed:b0Index.seed,axis:b0Index.axis,entrySide:b0Index.entrySide||null,sealedSides:b0Index.sealedSides||[],puzzle:b0Puzzle.puzzle,generatorVersion:b0Puzzle.generatorVersion||GENERATOR_VERSION,rev:b0Puzzle.metaRev,revAuthor:b0Puzzle.revAuthor||b0Index.metaRevAuthor||''});
if(!meta||!meta.chunks.some(([dx,dy])=>meta.x+dx===0&&meta.y+dy===0))throw new Error('The activated field contains an invalid B0 puzzle or origin.');validateImportedStateStrict(meta,b0State.value);return true;
}
async function deleteV2EpochBatch(epoch,limit=GC_BATCH_ROWS){
const stores=['boardIndex','boardPuzzles','boardStates','tombstonesV2','recoveryV2','outboxV2'],db=await openWorldDb(),worldTx=db.transaction('worlds','readonly'),worldDone=transactionDone(worldTx),world=await requestValue(worldTx.objectStore('worlds').get(epoch));await worldDone;if(!world)return{done:true,rows:0};
let storeIndex=Math.max(0,stores.indexOf(world.progress?.store));if(storeIndex<0)storeIndex=0;
for(;storeIndex<stores.length;storeIndex++){
const name=stores[storeIndex],tx=db.transaction([name,'worlds'],'readwrite'),done=transactionDone(tx),store=tx.objectStore(name),keys=await requestValue(store.getAllKeys(epochKeyRange(epoch),limit));
for(const key of keys)store.delete(key);
const next={...world,progress:{phase:'cleanup',store:name,lastKey:keys[keys.length-1]||world.progress?.lastKey||null,rows:(world.progress?.rows||0)+keys.length}};tx.objectStore('worlds').put(next);await done;
if(keys.length)return{done:false,rows:keys.length};world.progress=next.progress;
}
const finalTx=db.transaction('worlds','readwrite'),finalDone=transactionDone(finalTx);finalTx.objectStore('worlds').delete(epoch);await finalDone;return{done:true,rows:0};
}
async function collectV2Garbage(){
if(!idbAvailable||activeStorageFormat!==FIELD_STORAGE_FORMAT)return 0;
const db=await openWorldDb(),control=await readActiveWorldControl(db),tx=db.transaction(['worlds','recoveryV2'],'readonly'),done=transactionDone(tx),worlds=await requestValue(tx.objectStore('worlds').getAll()),pins=await requestValue(tx.objectStore('recoveryV2').getAll());await done;
const pinned=new Set(pins.filter(row=>String(row?.key||'').startsWith('pin:')&&validWorldEpoch(row?.value?.epoch||row?.value?.worldEpoch)).map(row=>row.value.epoch||row.value.worldEpoch)),cutoff=trustedNow()-24*60*60*1000,
target=worlds.find(world=>world.epoch!==control?.activeEpoch&&world.epoch!==control?.previousEpoch&&!pinned.has(world.epoch)&&(world.status==='garbage'||(world.status==='staging'||world.status==='ready')&&(world.createdAt||0)<cutoff));
if(FieldPersistence.cleanupTemporaryExports)await FieldPersistence.cleanupTemporaryExports({olderThanMs:24*60*60*1000}).catch(()=>{});
if(!target)return 0;const result=await deleteV2EpochBatch(target.epoch,GC_BATCH_ROWS);if(result.rows)perfCount('fieldEpochRowsCollected',result.rows);if(result.done)perfCount('fieldEpochsCollected');
setTimeout(()=>{void collectV2Garbage().catch(error=>console.warn('BEND FIELD: epoch cleanup deferred',error))},0);return result.rows;
}
async function writeStagedBoardBatch(epoch,batch){
if(!batch.length)return;
const db=await openWorldDb(),tx=db.transaction(['boardIndex','boardPuzzles','boardStates'],'readwrite'),done=transactionDone(tx),
indexStore=tx.objectStore('boardIndex'),puzzleStore=tx.objectStore('boardPuzzles'),stateStore=tx.objectStore('boardStates');
for(const{meta,state}of batch){indexStore.put(summarizeBoardV2(meta,state,epoch));puzzleStore.put(puzzleRecordV2(meta,epoch));stateStore.put(stateRecordV2(meta.id,state,epoch))}
await done;
}
function importedGlobalFromPortable(raw,epoch,{nextId,solved,boardScore}){
const result=defaultData();result.worldEpoch=epoch;result.gameplayVersion=GAMEPLAY_DATA_VERSION;
result.bonusEvents=normalizeBonusEvents(raw?.bonusEvents);result.bonusScore=bonusEventTotal(result.bonusEvents);result.bonusScoreVersion=SCORE_VERSION;
result.specialMechanicsSeen=normalizeSpecialMechanics(raw?.specialMechanicsSeen);
result.clockFloor=Math.max(0,Number(raw?.clockFloor)||0);result.lastSolveAt=Math.max(0,Number(raw?.lastSolveAt)||0);
result.timeAttack=normalizeTimeAttackRun(raw?.timeAttack);result.timeAttackCooldowns=normalizeTimeAttackCooldowns(raw?.timeAttackCooldowns);result.lastTimeAttack=normalizeTimeAttackResult(raw?.lastTimeAttack);
result.timeAttackSuggestionsDisabled=raw?.timeAttackSuggestionsDisabled===true;result.cursorStyle=typeof raw?.cursorStyle==='string'?raw.cursorStyle.slice(0,32):'default';result.scoreLensEnabled=raw?.scoreLensEnabled===true;
result.nextId=nextId;result.solved=solved;result.score=Math.min(MAX_SCORE,boardScore+result.bonusScore);result.globalRev=nextRevision();result.globalRevAuthor=sessionId;
result.cloudProfile=null;result.cloudRevision=0;result.cloudSyncPaused=true;result.cloudPending=normalizeCloudPending(null);result.updatedAt=trustedNow();return result;
}
async function activeWorldExpectation(){
const db=await openWorldDb(),control=await readActiveWorldControl(db);
if(!control||!validWorldEpoch(control.activeEpoch))throw new Error('The active field control record is missing.');
const tx=db.transaction('worlds','readonly'),done=transactionDone(tx),world=await requestValue(tx.objectStore('worlds').get(control.activeEpoch));await done;
return{epoch:control.activeEpoch,globalRev:world?.global?.globalRev||0,format:control.activeFormat};
}
async function preflightArchiveStorage(estimatedRawBytes){
if(!navigator.storage?.estimate)return true;
const estimate=await navigator.storage.estimate(),required=Math.max(32*1024*1024,Math.max(0,Number(estimatedRawBytes)||0)*2+32*1024*1024),
available=Math.max(0,(estimate.quota||0)-(estimate.usage||0));
if(estimate.quota&&available<required){const error=new Error('There is not enough browser storage to stage this field safely.');error.code='QUOTA_PREFLIGHT';throw error}
return true;
}
async function verifyStagedStorageEstimate(){
if(!navigator.storage?.estimate)return true;
const estimate=await navigator.storage.estimate(),quota=Number(estimate.quota)||0,usage=Number(estimate.usage)||0;
if(quota&&quota-usage<1024*1024){const error=new Error('There is not enough browser storage to activate the staged field safely.');error.code='QUOTA_POSTFLIGHT';throw error}
return true;
}
function assertCurrentArchive(manifest){
if(manifest?.saveSchema!==SAVE_SCHEMA||manifest?.gameplayVersion!==GAMEPLAY_DATA_VERSION||manifest?.worldGeneration!==WORLD_GENERATION)throw new Error('この版で作成したバックアップだけを読み込めます。');
return manifest;
}
async function stageArchiveV2(file,controller,expected){
if(!idbAvailable)throw new Error('IndexedDB is required to restore a field.');
const epoch=createWorldEpoch(),db=await openWorldDb(),occupied=new Set(),batch=[],batchLimit=256;
let batchBytes=0,globalRaw=null,boardCount=0,solved=0,boardScore=0,maxNumber=0,approximateBytes=0,manifest=null,minX=0,minY=0,maxX=1,maxY=1,boundsReady=false;
{const tx=db.transaction('worlds','readwrite'),done=transactionDone(tx);tx.objectStore('worlds').put({epoch,status:'staging',schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,global:null,boardCount:0,solvedCount:0,score:0,bounds:{minX:0,minY:0,maxX:1,maxY:1},approximateBytes:0,createdAt:trustedNow(),source:{kind:'import'},progress:{phase:'validate',rows:0}});await done}
const flush=async()=>{if(!batch.length)return;await writeStagedBoardBatch(epoch,batch);const tx=db.transaction('worlds','readwrite'),done=transactionDone(tx),row=await requestValue(tx.objectStore('worlds').get(epoch));tx.objectStore('worlds').put({...row,progress:{phase:'stage',lastKey:batch[batch.length-1].meta.id,rows:boardCount}});await done;batch.length=0;batchBytes=0};
try{
const result=await FieldPersistence.readArchive(file,{
signal:controller.signal,onProgress:progress=>{setSaveStatus('saving',`${progress.boardsDone}/${progress.boardsTotal}`);showStatus(`フィールドを検証中 ${progress.boardsDone}/${progress.boardsTotal}`,{retry:false,fresh:false})},
onManifest:async value=>{manifest=assertCurrentArchive(value);if(value.boardCount<1||value.boardCount>MAX_BOARDS)throw new Error('The archive board count is outside the supported range.');await preflightArchiveStorage(value.estimatedRawBytes)},
onGlobal:value=>{globalRaw=value},
onBoard:async record=>{
if(!isPlainObject(record.meta)||!isPlainObject(record.state)||record.meta.id&&record.meta.id!==record.id)throw new Error(`Invalid board record: ${record.id}`);
const meta=normalizeMeta(record.id,{...record.meta,id:record.id,rev:0,revAuthor:''});if(!meta)throw new Error(`Invalid puzzle metadata: ${record.id}`);
if(stableJson(portableMetaForArchive(meta))!==stableJson(record.meta))throw new Error(`The board metadata is not canonical: ${record.id}`);
for(const[dx,dy]of meta.chunks){const x=meta.x+dx,y=meta.y+dy,key=key2(x,y);if(occupied.has(key))throw new Error(`Overlapping board geometry: ${record.id}`);occupied.add(key);if(!boundsReady){minX=x;minY=y;maxX=x+1;maxY=y+1;boundsReady=true}else{minX=Math.min(minX,x);minY=Math.min(minY,y);maxX=Math.max(maxX,x+1);maxY=Math.max(maxY,y+1)}}
const state=validateImportedStateStrict(meta,{...record.state,rev:0,revAuthor:''});if(stableJson(portableStateForArchive(state))!==stableJson(record.state))throw new Error(`The board state is not canonical: ${record.id}`);
const revision=Math.min(Number.MAX_SAFE_INTEGER-2,Date.now()*1000+boardCount*2);meta.rev=revision;meta.revAuthor=sessionId;state.rev=revision+1;state.revAuthor=sessionId;
boardCount++;maxNumber=Math.max(maxNumber,Number(record.id.slice(1))||0);if(state.solved){solved++;boardScore=Math.min(MAX_SCORE,boardScore+state.scoreAwarded)}
const bytes=JSON.stringify(record).length;approximateBytes+=bytes;batchBytes+=bytes;batch.push({meta,state});if(batch.length>=batchLimit||batchBytes>=4*1024*1024)await flush();else if(boardCount%32===0)await new Promise(resolve=>setTimeout(resolve,0));
}
});
await flush();if(!manifest||!globalRaw||boardCount!==manifest.boardCount||!occupied.size)throw new Error('The archive did not produce a complete field.');
const imported=importedGlobalFromPortable(globalRaw,epoch,{nextId:maxNumber+1,solved,boardScore});if(stableJson(portableGlobalForArchive(imported))!==stableJson(globalRaw))throw new Error('The archive global record is not canonical.');
const provisional={epoch,status:'staging',schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,global:globalForStorage(imported),boardCount,solvedCount:solved,score:imported.score,bounds:{minX,minY,maxX,maxY},approximateBytes:result.decodedBytes||approximateBytes,createdAt:trustedNow(),source:{kind:'import',archiveCrc32:result.footer.crc32},progress:{phase:'verify',rows:boardCount}},
tx=db.transaction('worlds','readwrite'),done=transactionDone(tx);tx.objectStore('worlds').put(provisional);await done;
const verified=await validateStagedWorldV2(epoch,{expectedBoardCount:manifest.boardCount,strict:true,allowStaging:true});await verifyStagedStorageEstimate();const world={...provisional,status:'ready',boardCount:verified.boardCount,solvedCount:verified.solvedCount,score:verified.score,bounds:verified.bounds,approximateBytes:verified.approximateBytes,progress:null};
const readyTx=db.transaction('worlds','readwrite'),readyDone=transactionDone(readyTx);readyTx.objectStore('worlds').put(world);await readyDone;return{epoch,expected,world,manifest};
}catch(error){await deleteV2Epoch(epoch).catch(()=>{});throw error}
}
async function stageRecoverySnapshotV2(snapshot,expected,controller){
const epoch=createWorldEpoch(),ids=Object.keys(snapshot.metas).sort((a,b)=>(Number(a.slice(1))||0)-(Number(b.slice(1))||0));await preflightArchiveStorage(Math.max(1,ids.length)*1024);
const db=await openWorldDb(),portableGlobal=portableGlobalForArchive(snapshot),occupied=new Set();let solved=0,boardScore=0,maxNumber=0,approximateBytes=0;
try{
const initialTx=db.transaction('worlds','readwrite'),initialDone=transactionDone(initialTx);initialTx.objectStore('worlds').put({epoch,status:'staging',schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,global:null,boardCount:0,createdAt:trustedNow(),source:{kind:'import'},progress:{phase:'stage',rows:0}});await initialDone;
for(let offset=0;offset<ids.length;offset+=64){
if(controller.signal.aborted)throw controller.signal.reason||new DOMException('Canceled','AbortError');
const batch=ids.slice(offset,offset+64).map((id,position)=>{const meta=normalizeMeta(id,{...metaForStorage(snapshot.metas[id]),rev:0,revAuthor:''});if(!meta)throw new Error(`Invalid recovery board: ${id}`);for(const[dx,dy]of meta.chunks){const key=key2(meta.x+dx,meta.y+dy);if(occupied.has(key))throw new Error(`Overlapping recovery board: ${id}`);occupied.add(key)}const state=normalizeState(stateForStorage(snapshot.states[id]||normalizeState(null))),revision=Math.min(Number.MAX_SAFE_INTEGER-2,Date.now()*1000+(offset+position)*2);meta.rev=revision;meta.revAuthor=sessionId;state.rev=revision+1;state.revAuthor=sessionId;if(state.solved){solved++;boardScore=Math.min(MAX_SCORE,boardScore+state.scoreAwarded)}maxNumber=Math.max(maxNumber,Number(id.slice(1))||0);approximateBytes+=JSON.stringify(meta).length+JSON.stringify(state).length;return{meta,state}});
await writeStagedBoardBatch(epoch,batch);const progressTx=db.transaction('worlds','readwrite'),progressDone=transactionDone(progressTx),row=await requestValue(progressTx.objectStore('worlds').get(epoch));progressTx.objectStore('worlds').put({...row,progress:{phase:'stage',lastKey:batch[batch.length-1]?.meta.id||null,rows:Math.min(ids.length,offset+batch.length)}});await progressDone;setSaveStatus('saving',`${Math.min(ids.length,offset+batch.length)}/${ids.length}`);await new Promise(resolve=>setTimeout(resolve,0));
}
const imported=importedGlobalFromPortable(portableGlobal,epoch,{nextId:maxNumber+1,solved,boardScore}),provisional={epoch,status:'staging',schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,global:globalForStorage(imported),boardCount:ids.length,solvedCount:solved,score:imported.score,bounds:fieldBoundsFromMetas(snapshot.metas),approximateBytes,createdAt:trustedNow(),source:{kind:'import'},progress:{phase:'verify',rows:ids.length}},
tx=db.transaction('worlds','readwrite'),done=transactionDone(tx);tx.objectStore('worlds').put(provisional);await done;
const verified=await validateStagedWorldV2(epoch,{expectedBoardCount:ids.length,strict:false,allowStaging:true});await verifyStagedStorageEstimate();const world={...provisional,status:'ready',boardCount:verified.boardCount,solvedCount:verified.solvedCount,score:verified.score,bounds:verified.bounds,approximateBytes:verified.approximateBytes,progress:null};
const readyTx=db.transaction('worlds','readwrite'),readyDone=transactionDone(readyTx);readyTx.objectStore('worlds').put(world);await readyDone;return{epoch,expected,world,recovery:true};
}catch(error){await deleteV2Epoch(epoch).catch(()=>{});throw error}
}
async function activateReadyWorldV2(staged,{kind='import'}={}){
return withWorldMutationLock(async()=>{
if(hasPendingPersistence())await persistDirtyToDb({skipCloud:true});
const db=await openWorldDb(),tx=db.transaction(['control','worlds'],'readwrite'),done=transactionDone(tx),controlStore=tx.objectStore('control'),worldsStore=tx.objectStore('worlds'),
[control,currentWorld,readyWorld]=await Promise.all([requestValue(controlStore.get('active')),requestValue(worldsStore.get(staged.expected.epoch)),requestValue(worldsStore.get(staged.epoch))]);
const olderPrevious=control?.previousEpoch&&control.previousEpoch!==staged.expected.epoch?await requestValue(worldsStore.get(control.previousEpoch)):null;
if(!control||control.activeEpoch!==staged.expected.epoch||control.activeFormat!==FIELD_STORAGE_FORMAT||(currentWorld?.global?.globalRev||0)!==staged.expected.globalRev){try{tx.abort()}catch(_){}await done.catch(()=>{});throw new Error('The active field changed while the replacement was being validated. Please try again.')}
if(!readyWorld||readyWorld.status!=='ready'){try{tx.abort()}catch(_){}await done.catch(()=>{});throw new Error('The staged field is not ready to activate.')}
if(olderPrevious){olderPrevious.status='garbage';olderPrevious.progress={phase:'cleanup',store:'boardIndex',rows:0};worldsStore.put(olderPrevious)}if(currentWorld){currentWorld.status='rollback';currentWorld.progress=null;worldsStore.put(currentWorld)}readyWorld.status='active';readyWorld.activatedAt=trustedNow();
const nextControl={key:'active',activeFormat:FIELD_STORAGE_FORMAT,activeEpoch:staged.epoch,previousEpoch:staged.expected.epoch,activationId:`${kind}:${sessionId}:${Date.now()}`,activationVerified:false,switchedAt:trustedNow()};
worldsStore.put(readyWorld);controlStore.put(nextControl);
await done;return nextControl;
});
}
async function activateStagedWorldV2(staged,{kind='import'}={}){
let control;try{control=await activateReadyWorldV2(staged,{kind})}catch(error){await deleteV2Epoch(staged?.epoch).catch(()=>{});throw error}lifecyclePersistenceSuppressed=true;clearTimeout(saveTimer);saveTimer=null;cancelMirrorCheckpoint(true);clearCompactMirror();clearRecoveryJournalKeys();safeLocalRemove(recoveryStorageKey);try{sessionStorage.removeItem(recoveryStorageKey)}catch(_){}volatileRecovery=null;safeLocalRemove(storageRevisionKey);rememberWorldEpoch(control.activeEpoch);announceWorldReplacement(control.activeEpoch);location.reload();return true;
}
async function importSaveFile(file){
if(!file)return false;
if(!idbAvailable)throw new Error('IndexedDB is required to restore a field.');
const controller=new AbortController();activeArchiveController=controller;setFieldArchiveBusy(true,'\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u8aad\u307f\u8fbc\u3093\u3067\u3044\u307e\u3059\u3002');
try{
if(!await flushSave())throw new Error('The current field could not be checkpointed before import.');
if(activeStorageFormat!==FIELD_STORAGE_FORMAT)throw new Error('現行の保存形式ではありません。');
const expected=await activeWorldExpectation(),summary=assertCurrentArchive(await FieldPersistence.inspectArchive(file,{signal:controller.signal}));
if(!Number.isSafeInteger(summary.boardCount)||summary.boardCount<1||summary.boardCount>MAX_BOARDS)throw new Error('The archive board count is outside the supported range.');
if(!confirm(`\u73fe\u5728\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092${summary.boardCount}\u76e4\u9762\u306e\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u3067\u7f6e\u304d\u63db\u3048\u307e\u3059\u304b\uff1f`))return false;
const staged=await stageArchiveV2(file,controller,expected);
return await activateStagedWorldV2(staged);
}finally{if(!lifecyclePersistenceSuppressed){activeArchiveController=null;setFieldArchiveBusy(false)}}
}
const importFileEl=document.querySelector('#importFile'),importBtn=document.querySelector('#importBtn');
if(importBtn&&importFileEl){
importBtn.onclick=()=>{importFileEl.value='';importFileEl.click()};
importFileEl.onchange=async()=>{try{await importSaveFile(importFileEl.files?.[0])}catch(error){showStatus(`\u4fdd\u5b58\u30c7\u30fc\u30bf\u3092\u8aad\u307f\u8fbc\u3081\u307e\u305b\u3093\u3067\u3057\u305f\u3002 ${error?.message||error}`,{retry:false,fresh:false});toast('\u4fdd\u5b58\u30c7\u30fc\u30bf\u3092\u8aad\u307f\u8fbc\u3081\u307e\u305b\u3093\u3067\u3057\u305f\u3002')}};
}
document.querySelector('#retryBtn').onclick=()=>{void runStatusRetry()};
function announceWorldReplacement(){}
function checkpointForLifecycle(){if(lifecyclePersistenceSuppressed)return;writeDirtyRecoveryJournal();void flushSave({lifecycle:true})}
window.addEventListener('pagehide',checkpointForLifecycle);
window.addEventListener('pagehide',()=>{skipCompletionVisuals();cleanupGemEffects();cancelReactionRenderScheduler(true);stopAuroraRgbAnimation()});
document.addEventListener('visibilitychange',()=>{const hidden=document.visibilityState==='hidden';document.body.classList.toggle('effects-paused',hidden);if(hidden){cancelReactionGesture();hideRealtimeCursor();cancelReactionRenderScheduler(true);stopAuroraRgbAnimation();clearTimeout(realtimeHeartbeatTimer);realtimeHeartbeatTimer=0;clearTimeout(realtimePollTimer);realtimePollTimer=0;pauseNoiseBackground();checkpointForLifecycle();void pushCloudPending()}else{scheduleNoiseBackground(true);resumeTimeAttackTimer();connectRealtime();scheduleRealtimeViewport(true);scheduleRealtimeHeartbeat();schedulePresenceRender(true);updateAuroraAnimationState();scheduleReactionRender();void pullCloudWorld();scheduleMirrorCheckpoint()}});
const cloudBtn=document.querySelector('#cloudBtn');
function emptyCloudPending(){return{metaIds:new Set(),stateIds:new Set(),deleted:new Set(),globalChanged:false}}
let cloudAvailable=false,cloudPushTimer=null,cloudPushPending=emptyCloudPending(),cloudSyncing=false,cloudCheckpointRetryTimer=0,worldPollTimer=0,remoteWorldPullTimer=0,remoteWorldRevision=0,lastCloudWorldGlobalSignature='';
const remotePlayers=new Map(),boardClaims=new Map(),realtimeClaimRequests=new Map(),remoteCursorImageCache=new Map(),realtimeHeldPointers=new Set(),realtimeReactions=new Map(),pendingRealtimeReactionMessages=[];
let realtimeSocket=null,realtimeReady=false,realtimePresenceId=null,realtimeClaimTtlMs=5*60*1000,realtimeReconnectTimer=0,realtimeViewportTimer=0,realtimeCursorTimer=0,realtimeHeartbeatTimer=0,realtimePollTimer=0,realtimePollSequence=0,realtimeTransport='none',realtimeHttpConnecting=false,realtimeHttpQueue=Promise.resolve(),realtimeWebSocketFailures=0,realtimeLastCursorSentAt=0,realtimeLastCursorSampleAt=0,realtimePendingCursor=null,realtimePendingCursorClient=null,realtimeRequestSequence=0,realtimeOwnClaimBoardId=null,realtimeLastClaimTouchAt=0,presenceFrame=0,presenceDelayTimer=0,presenceLastDraw=0,presenceDirty=true,presenceLastTimestamp=0,presenceCameraSnapshot=null,reactionFrame=0,reactionDelayTimer=0,reactionWatchdogTimer=0,reactionCalibrationFrame=0,reactionVsyncInterval=1000/60,reactionVsyncCalibrated=false,reactionLastDraw=0,reactionLastMetricFrame=0,reactionDirty=true,reactionCameraSnapshot=null,reactionGesture=null,reactionSequence=0;
function realtimeWebSocketUrl(){const endpoint=new URL(cloudEndpointUrl('/api/realtime'));endpoint.protocol=endpoint.protocol==='https:'?'wss:':'ws:';return endpoint.href}
function enqueueRealtimeHttp(task){const run=realtimeHttpQueue.then(task,task);realtimeHttpQueue=run.then(()=>undefined,()=>undefined);return run}
function applyRealtimeHttpEnvelope(envelope){if(!envelope||typeof envelope!=='object')return false;if(Number.isFinite(envelope.serverTime))serverClockOffset=envelope.serverTime-Date.now();if(Number.isFinite(envelope.sequence))realtimePollSequence=Math.max(0,Number(envelope.sequence));for(const message of Array.isArray(envelope.messages)?envelope.messages:[])handleRealtimeMessage(message);if(envelope.eventGap===true&&realtimeReady)realtimeSend({type:'snapshot-request'});return true}
function scheduleRealtimeHttpPoll(delay=cloudApiUsesPhpBridge()?PHP_REALTIME_POLL_INTERVAL:50){clearTimeout(realtimePollTimer);realtimePollTimer=0;if(realtimeTransport!=='http-poll'||!realtimePresenceId||document.visibilityState==='hidden')return false;realtimePollTimer=setTimeout(()=>{realtimePollTimer=0;void enqueueRealtimeHttp(async()=>{try{const bridge=cloudApiUsesPhpBridge(),query=new URLSearchParams({presenceId:realtimePresenceId,after:String(realtimePollSequence),wait:bridge?'0':'20000'}),result=await fetchJson(`/api/realtime/poll?${query}`,{headers:cloudAuthHeaders()},bridge?5000:25000);applyRealtimeHttpEnvelope(result);scheduleRealtimeHttpPoll(bridge?PHP_REALTIME_POLL_INTERVAL:50)}catch(error){if(error?.status===410){realtimeReady=false;realtimePresenceId=null;realtimeTransport='none'}scheduleRealtimeReconnect(1200)}})},Math.max(25,delay));return true}
function realtimeSend(message){
if(!realtimeReady)return false;
if(realtimeTransport==='http-poll'){
const presenceId=realtimePresenceId;if(!presenceId)return false;
void enqueueRealtimeHttp(async()=>{try{const result=await fetchJson('/api/realtime/send',{method:'POST',headers:cloudAuthHeaders(),body:JSON.stringify({presenceId,message,afterSequence:realtimePollSequence})},5000);applyRealtimeHttpEnvelope(result);scheduleRealtimeHttpPoll(250)}catch(error){if(error?.status===410){realtimeReady=false;realtimePresenceId=null;realtimeTransport='none'}scheduleRealtimeReconnect(1200)}});return true;
}
if(realtimeSocket?.readyState!==WebSocket.OPEN)return false;try{realtimeSocket.send(JSON.stringify(message));return true}catch(_){return false}
}
function disconnectRealtimeForLifecycle(){
const presenceId=realtimePresenceId,transport=realtimeTransport,socket=realtimeSocket;
realtimePresenceId=null;realtimeReady=false;realtimeTransport='none';realtimeSocket=null;
if(transport==='http-poll'&&presenceId&&data.cloudProfile){
try{void fetch(cloudEndpointUrl('/api/realtime/disconnect'),{method:'POST',headers:{...cloudAuthHeaders(),'content-type':'application/json'},body:JSON.stringify({presenceId}),keepalive:true,cache:'no-store'})}catch(_){}
}else if(socket)try{socket.close(1000,'Page closed')}catch(_){}
}
window.addEventListener('pagehide',disconnectRealtimeForLifecycle);
function sendOrQueueRealtimeReaction(reaction){
if(!reaction||reaction.expiresAt<=Date.now())return false;const message={type:'reaction',id:reaction.id,emoji:reaction.emoji,style:reaction.style,x:reaction.x,y:reaction.y};
if(realtimeSend(message))return true;if(!pendingRealtimeReactionMessages.some(entry=>entry.id===reaction.id))pendingRealtimeReactionMessages.push({...message,expiresAt:reaction.expiresAt});while(pendingRealtimeReactionMessages.length>12)pendingRealtimeReactionMessages.shift();connectRealtime();return false;
}
function flushPendingRealtimeReactions(){
if(!realtimeReady||!pendingRealtimeReactionMessages.length)return 0;const now=Date.now(),pending=pendingRealtimeReactionMessages.splice(0),remaining=[];let sent=0;
for(const message of pending){if(message.expiresAt<=now)continue;const{expiresAt,...payload}=message;if(realtimeSend(payload))sent++;else remaining.push(message)}pendingRealtimeReactionMessages.push(...remaining);return sent;
}
function realtimeViewportBounds(){const rect=getViewportRect(),topLeft=worldUnitAtClient(rect.left,rect.top),bottomRight=worldUnitAtClient(rect.right,rect.bottom);return{minX:Math.min(topLeft[0],bottomRight[0]),minY:Math.min(topLeft[1],bottomRight[1]),maxX:Math.max(topLeft[0],bottomRight[0]),maxY:Math.max(topLeft[1],bottomRight[1])}}
function sendRealtimeViewport(){realtimeViewportTimer=0;if(!realtimeReady)return false;const bounds=realtimeViewportBounds();return realtimeSend({type:'viewport',...bounds})}
function scheduleRealtimeViewport(immediate=false){if(!realtimeReady)return false;if(immediate){clearTimeout(realtimeViewportTimer);realtimeViewportTimer=0;return sendRealtimeViewport()}if(realtimeViewportTimer)return true;realtimeViewportTimer=setTimeout(sendRealtimeViewport,REALTIME_VIEWPORT_INTERVAL);return true}
function scheduleRealtimeHeartbeat(){clearTimeout(realtimeHeartbeatTimer);realtimeHeartbeatTimer=0;if(!realtimeReady||document.visibilityState==='hidden')return false;realtimeHeartbeatTimer=setTimeout(()=>{realtimeHeartbeatTimer=0;if(realtimePendingCursor)realtimeSend({type:'cursor',...realtimePendingCursor,vx:0,vy:0,sentAt:Date.now()});scheduleRealtimeHeartbeat()},REALTIME_CURSOR_HEARTBEAT_INTERVAL);return true}
function queueRealtimeCursor(event){
if(!realtimeReady||event.pointerType==='touch'||document.body.classList.contains('is-drawing')||!viewport.contains(event.target))return;
realtimePendingCursorClient={clientX:event.clientX,clientY:event.clientY,inputAt:Number(event.timeStamp)||perfNow()};
const transportInterval=realtimeTransport==='http-poll'?(cloudApiUsesPhpBridge()?PHP_REALTIME_CURSOR_INTERVAL:250):REALTIME_CURSOR_INTERVAL,wait=transportInterval-(perfNow()-realtimeLastCursorSentAt);if(realtimeCursorTimer)return;
realtimeCursorTimer=setTimeout(()=>{
realtimeCursorTimer=0;const client=realtimePendingCursorClient;if(!client||!realtimeReady)return;
const point=worldUnitAtClient(client.clientX,client.clientY),previous=realtimePendingCursor,now=perfNow(),elapsed=Math.max(1,now-(realtimeLastCursorSampleAt||now)),
next={x:point[0],y:point[1],vx:previous?(point[0]-previous.x)*1000/elapsed:0,vy:previous?(point[1]-previous.y)*1000/elapsed:0,sentAt:Date.now(),cursorStyle:data.cursorStyle||'default'};
realtimeLastCursorSampleAt=now;
realtimePendingCursor=next;realtimeLastCursorSentAt=perfNow();
if(previous&&previous.cursorStyle===next.cursorStyle&&Math.abs(previous.x-next.x)<.002&&Math.abs(previous.y-next.y)<.002)return;
realtimeSend({type:'cursor',...next});
},Math.max(0,wait));
}
function hideRealtimeCursor(){clearTimeout(realtimeCursorTimer);realtimeCursorTimer=0;realtimeLastCursorSampleAt=0;realtimePendingCursorClient=null;realtimePendingCursor=null;if(realtimeReady)realtimeSend({type:'cursor-hide'})}
function remoteCursorImage(item){
if(!item?.flagAsset)return null;let entry=remoteCursorImageCache.get(item.flagAsset);if(entry){remoteCursorImageCache.delete(item.flagAsset);remoteCursorImageCache.set(item.flagAsset,entry);return entry.image}
const image=new Image();entry={image,ready:false};remoteCursorImageCache.set(item.flagAsset,entry);while(remoteCursorImageCache.size>48)remoteCursorImageCache.delete(remoteCursorImageCache.keys().next().value);image.onload=()=>{entry.ready=true;schedulePresenceRender(true)};image.onerror=()=>{entry.failed=true};image.src=item.flagAsset;return image;
}
function roundedCanvasRect(context,x,y,width,height,radius){const r=Math.max(0,Math.min(radius,width/2,height/2));context.beginPath();context.moveTo(x+r,y);context.arcTo(x+width,y,x+width,y+height,r);context.arcTo(x+width,y+height,x,y+height,r);context.arcTo(x,y+height,x,y,r);context.arcTo(x,y,x+width,y,r);context.closePath()}
function drawDefaultRemoteCursor(context,x,y){context.save();context.translate(x,y);context.beginPath();context.moveTo(-6,-10);context.lineTo(9,1);context.lineTo(2,3);context.lineTo(6,11);context.lineTo(1,13);context.lineTo(-3,5);context.lineTo(-8,10);context.closePath();context.lineJoin='round';context.lineWidth=4;context.strokeStyle='rgba(8,12,15,.95)';context.stroke();context.lineWidth=2;context.strokeStyle='#fff';context.stroke();context.fillStyle='#d9f06f';context.fill();context.restore()}
function drawRemoteCursorGlyph(context,player,x,y){
const item=cursorModel.item(player.cursorStyle);
if(!item){drawDefaultRemoteCursor(context,x,y);return}
if(item.flagAsset){const entry=remoteCursorImageCache.get(item.flagAsset),image=remoteCursorImage(item);if(entry?.ready){context.save();context.translate(x,y);roundedCanvasRect(context,-15,-11,30,22,4);context.clip();context.drawImage(image,-17,-13,34,26);context.restore();context.strokeStyle='rgba(255,255,255,.9)';context.lineWidth=1.5;roundedCanvasRect(context,x-15,y-11,30,22,4);context.stroke()}else drawDefaultRemoteCursor(context,x,y);return}
context.save();context.font='25px "Segoe UI Emoji","Apple Color Emoji","Noto Color Emoji",sans-serif';context.textAlign='center';context.textBaseline='middle';context.shadowColor='rgba(0,0,0,.8)';context.shadowBlur=4;context.fillText(item.cursorEmoji||'•',x,y);context.restore();
}
function drawRemotePlayerName(context,player,x,y){
const name=String(player.name||'旅人').slice(0,24);context.save();context.font='700 10px DotGothic16Local,monospace';context.textBaseline='middle';const width=Math.ceil(context.measureText(name).width)+10,left=x+12,top=y+11;roundedCanvasRect(context,left,top,width,18,3);context.fillStyle='rgba(8,12,15,.88)';context.fill();context.strokeStyle='rgba(255,255,255,.3)';context.lineWidth=1;context.stroke();context.fillStyle='#fff';context.fillText(name,left+5,top+9);context.restore();
}
function onlineLayerCameraSnapshot(){const scale=Math.max(MIN_CAMERA_SCALE,cam.scale);return{scale,offsetX:cam.x-renderOriginX*UNIT*scale,offsetY:cam.y-renderOriginY*UNIT*scale}}
function shiftCameraBoundCanvas(canvas,snapshot){
if(!canvas||!snapshot)return false;const current=onlineLayerCameraSnapshot(),ratio=current.scale/snapshot.scale,
tx=current.offsetX-ratio*snapshot.offsetX,ty=current.offsetY-ratio*snapshot.offsetY;
canvas.style.transform=`matrix(${ratio.toFixed(6)},0,0,${ratio.toFixed(6)},${tx.toFixed(2)},${ty.toFixed(2)})`;return true;
}
function shiftOnlineLayersForCamera(){
if(remotePlayers.size)shiftCameraBoundCanvas(presenceCanvas,presenceCameraSnapshot);
if(realtimeReactions.size)shiftCameraBoundCanvas(reactionCanvas,reactionCameraSnapshot);
}
function redrawOnlineLayersAfterCamera(){
// Keep the camera compensation transform until the fresh frame is painted.
// Clearing it here exposes the stale bitmap for one frame and causes a jump.
if(remotePlayers.size||presenceDirty)schedulePresenceRender(true);if(realtimeReactions.size||reactionDirty)scheduleReactionRender(true);
}
function schedulePresenceRender(markDirty=false){
if(markDirty)presenceDirty=true;if(!presenceCanvas||presenceFrame||(!presenceDirty&&!remotePlayers.size))return;
const step=timestamp=>{if(!presenceFrame)return;if(timestamp-presenceLastDraw<AUXILIARY_FRAME_INTERVAL-1){presenceFrame=requestAnimationFrame(step);return}presenceFrame=0;presenceLastDraw=timestamp;markVisualFrame(timestamp);drawPresenceLayer(timestamp)};
presenceFrame=requestAnimationFrame(step);
}
function drawPresenceLayer(timestamp=perfNow()){
if(!presenceCanvas)return;const rect=getViewportRect(),width=Math.max(1,Math.round(rect.width)),height=Math.max(1,Math.round(rect.height)),dpr=1,pixelWidth=Math.round(width*dpr),pixelHeight=Math.round(height*dpr);
if(presenceCanvas.width!==pixelWidth||presenceCanvas.height!==pixelHeight){presenceCanvas.width=pixelWidth;presenceCanvas.height=pixelHeight;presenceDirty=true}
presenceCanvas.style.transform='none';presenceCameraSnapshot=onlineLayerCameraSnapshot();
const context=presenceCanvas.getContext('2d');context.setTransform(dpr,0,0,dpr,0,0);context.clearRect(0,0,width,height);const now=Date.now(),dt=Math.min(80,Math.max(1,timestamp-(presenceLastTimestamp||timestamp-33)));presenceLastTimestamp=timestamp;let animate=false,playersRemoved=false;
for(const[presenceId,player]of remotePlayers){if(now-(player.lastSeen||0)>REALTIME_PLAYER_STALE_MS){remotePlayers.delete(presenceId);playersRemoved=true;continue}if(!Number.isFinite(player.currentX)){player.currentX=player.targetX;player.currentY=player.targetY}const alpha=1-Math.exp(-dt/45);player.currentX+=(player.targetX-player.currentX)*alpha;player.currentY+=(player.targetY-player.currentY)*alpha;if(Math.abs(player.targetX-player.currentX)>.001||Math.abs(player.targetY-player.currentY)>.001)animate=true;const x=(player.currentX-renderOriginX)*UNIT*cam.scale+cam.x,y=(player.currentY-renderOriginY)*UNIT*cam.scale+cam.y;if(x<-80||y<-80||x>width+120||y>height+80)continue;drawRemoteCursorGlyph(context,player,x,y);drawRemotePlayerName(context,player,x,y)}
presenceDirty=false;if(animate)schedulePresenceRender();else if(remotePlayers.size&&!presenceDelayTimer)presenceDelayTimer=setTimeout(()=>{presenceDelayTimer=0;schedulePresenceRender(true)},1000);if(playersRemoved)scheduleMinimap(true);
}
function applyRemotePlayer(raw){
if(!raw||raw.presenceId===realtimePresenceId||!Number.isFinite(raw.x)||!Number.isFinite(raw.y))return false;
const vx=Number.isFinite(raw.vx)?raw.vx:0,vy=Number.isFinite(raw.vy)?raw.vy:0,lead=.035,previous=remotePlayers.get(raw.presenceId)||{currentX:raw.x,currentY:raw.y};
previous.presenceId=raw.presenceId;previous.playerId=raw.playerId;previous.name=String(raw.name||'旅人').slice(0,24);previous.cursorStyle=String(raw.cursorStyle||'default');previous.targetX=raw.x+vx*lead;previous.targetY=raw.y+vy*lead;previous.vx=vx;previous.vy=vy;previous.sentAt=Number(raw.sentAt)||0;previous.lastSeen=Date.now();remotePlayers.set(raw.presenceId,previous);schedulePresenceRender(true);scheduleMinimap(true);return true;
}
function reactionSeed(id,index=0){let value=0x811c9dc5;for(const character of String(id||''))value=Math.imul(value^character.charCodeAt(0),0x01000193);return hash32(value^Math.imul(index+1,0x9e3779b1))}
function reactionUnit(seed,index=0){return(reactionSeed(seed,index)>>>0)/0xffffffff}
function reactionDurationForStyle(style){return style==='comet'?3000:style==='firework'?3400:style==='orbit'?3200:style==='giant'||style==='laser'?2700:1050}
const REACTION_GLYPH_CACHE_MAX_ENTRIES=160,REACTION_GLYPH_CACHE_MAX_BYTES=12*1024*1024,REACTION_STATIC_PATH_CACHE_MAX_ENTRIES=32,
REACTION_FONT='"Segoe UI Emoji","Apple Color Emoji","Noto Color Emoji",sans-serif',
REACTION_LASER_COLORS=Object.freeze(['#55efff','#ff4fd8','#fff56b','#8dff76','#9d75ff','#ff6b53']),
REACTION_ORBIT_NEBULA_PATH_BUILDER=()=>{const path=new Path2D();path.arc(0,0,250,0,Math.PI*2);return path},
REACTION_ORBIT_RING_PATH_BUILDERS=Object.freeze(Array.from({length:5},(_,ring)=>()=>{const path=new Path2D();path.ellipse(0,0,118+ring*15,37+ring*8,0,0,Math.PI*2);return path})),
REACTION_FIREWORK_LAUNCH_PATH_BUILDERS=Object.freeze(Array.from({length:12},(_,index)=>()=>{const p=index/12,path=new Path2D();path.arc(Math.sin(index*2.1)*5,index*12,2.5+(1-p)*2.5,0,Math.PI*2);return path}));
const reactionGlyphCache=new Map(),reactionStaticPathCache=new Map();
let reactionGlyphCacheBytes=0,reactionGlyphCacheBypass=0,reactionStaticPathCacheBypass=0,reactionNextDrawAt=0;
function updateReactionCacheGauges(){
perfGauge('reactionGlyphCacheEntries',reactionGlyphCache.size);perfGauge('reactionGlyphCacheBytes',reactionGlyphCacheBytes);perfGauge('reactionStaticPathCacheEntries',reactionStaticPathCache.size);
}
function clearReactionGlyphCache(){
reactionGlyphCache.clear();reactionGlyphCacheBytes=0;updateReactionCacheGauges();
}
function clearReactionStaticPathCache(){reactionStaticPathCache.clear();updateReactionCacheGauges()}
function clearReactionEffectCaches(){clearReactionGlyphCache();clearReactionStaticPathCache()}
function reactionStaticPath(key,builder){
if(reactionStaticPathCacheBypass||typeof Path2D!=='function')return null;const cached=reactionStaticPathCache.get(key);
if(cached){reactionStaticPathCache.delete(key);reactionStaticPathCache.set(key,cached);perfCount('reactionStaticPathCacheHits');return cached}
perfCount('reactionStaticPathCacheMisses');let path;try{path=builder()}catch(_){return null}reactionStaticPathCache.set(key,path);
while(reactionStaticPathCache.size>REACTION_STATIC_PATH_CACHE_MAX_ENTRIES){const oldestKey=reactionStaticPathCache.keys().next().value;reactionStaticPathCache.delete(oldestKey);perfCount('reactionStaticPathCacheEvictions')}
updateReactionCacheGauges();return path;
}
function reactionOrbitNebulaPath(){return reactionStaticPath('orbit.nebula',REACTION_ORBIT_NEBULA_PATH_BUILDER)}
function reactionOrbitRingPath(ring){return reactionStaticPath(`orbit.ring.${ring}`,REACTION_ORBIT_RING_PATH_BUILDERS[ring])}
function reactionFireworkLaunchPath(index){return reactionStaticPath(`firework.launch.${index}`,REACTION_FIREWORK_LAUNCH_PATH_BUILDERS[index])}
function createReactionGlyphCanvas(width,height){
if(typeof OffscreenCanvas==='function')return new OffscreenCanvas(width,height);
const canvas=document.createElement('canvas');canvas.width=width;canvas.height=height;return canvas;
}
function reactionGlyphCacheKey(context,emoji,size){
if(typeof context.fillStyle!=='string'||!Number.isFinite(size)||size<=0)return'';
return`${emoji}\u0000${size}\u0000${context.fillStyle}\u0000${globalThis.devicePixelRatio||1}`;
}
function createReactionGlyphSprite(context,emoji,size){
const extent=Math.max(32,Math.ceil(size*2.1)),dimension=extent+(extent%2),
canvas=createReactionGlyphCanvas(dimension,dimension),spriteContext=canvas.getContext('2d',{alpha:true});
if(!spriteContext)return null;
spriteContext.translate(dimension/2,dimension/2);spriteContext.font=`${size}px ${REACTION_FONT}`;spriteContext.textAlign='center';spriteContext.textBaseline='middle';
spriteContext.fillStyle=context.fillStyle;spriteContext.fillText(emoji,0,0);
return{canvas,width:dimension,height:dimension,bytes:dimension*dimension*4};
}
function reactionGlyphSprite(context,emoji,size){
if(reactionGlyphCacheBypass)return null;
const key=reactionGlyphCacheKey(context,emoji,size);if(!key)return null;
const cached=reactionGlyphCache.get(key);
if(cached){reactionGlyphCache.delete(key);reactionGlyphCache.set(key,cached);perfCount('reactionGlyphCacheHits');return cached}
perfCount('reactionGlyphCacheMisses');
const sprite=createReactionGlyphSprite(context,emoji,size);if(!sprite)return null;
reactionGlyphCache.set(key,sprite);reactionGlyphCacheBytes+=sprite.bytes;
while(reactionGlyphCache.size>REACTION_GLYPH_CACHE_MAX_ENTRIES||reactionGlyphCacheBytes>REACTION_GLYPH_CACHE_MAX_BYTES){
const oldestKey=reactionGlyphCache.keys().next().value,oldest=reactionGlyphCache.get(oldestKey);reactionGlyphCache.delete(oldestKey);reactionGlyphCacheBytes=Math.max(0,reactionGlyphCacheBytes-(oldest?.bytes||0));perfCount('reactionGlyphCacheEvictions');
}
updateReactionCacheGauges();return sprite;
}
function reactionGlyphWarmSizes(style){
return style==='giant'?[[142,1]]:style==='laser'?[[60,1]]:style==='orbit'?[[24,.78],[27,.78],[30,.78],[33,.78],[112,1]]:style==='firework'?[[21,1],[24,1],[28,1],[38,1]]:style==='comet'?[[52,1],[19,.62],[22,.62],[25,.62],[28,.62]]:[[40,1]];
}
function warmReactionGlyphCache(emoji,style){
if(!REACTION_EMOJIS.includes(emoji))return false;
const canvas=createReactionGlyphCanvas(1,1),context=canvas.getContext('2d',{alpha:true});if(!context)return false;for(const[size]of reactionGlyphWarmSizes(style))reactionGlyphSprite(context,emoji,size);
if(style==='orbit'){reactionOrbitNebulaPath();for(let ring=0;ring<5;ring++)reactionOrbitRingPath(ring)}else if(style==='firework')for(let index=0;index<12;index++)reactionFireworkLaunchPath(index);return true;
}
function prewarmReactionGlyphs(emoji,style){
if(!REACTION_EMOJIS.includes(emoji))return;
const run=()=>warmReactionGlyphCache(emoji,style);
if(typeof requestIdleCallback==='function')requestIdleCallback(run,{timeout:900});else setTimeout(run,250);
}
document.fonts?.addEventListener?.('loadingdone',clearReactionGlyphCache);
function preparedReactionBurst(seed,count){
const rays=[];
for(let index=0;index<count;index++){
const angle=reactionUnit(seed,index)*Math.PI*2;
rays.push({cos:Math.cos(angle),sin:Math.sin(angle),radiusFactor:.25+.75*reactionUnit(seed,index+100),innerOffset:18+28*reactionUnit(seed,index+200),hue:Math.round(reactionUnit(seed,index+300)*360),lineWidth:1.5+3*reactionUnit(seed,index+400)});
}
return rays;
}
function preparedReactionCracks(seed){
const branches=[];
for(let branch=0;branch<13;branch++){
let angle=branch*Math.PI*2/13+(reactionUnit(seed,branch)-.5)*.28;
const item={x:Math.cos(angle)*28,y:Math.sin(angle)*20,alpha:.42+.45*reactionUnit(seed,branch+350),lineWidth:1.4+reactionUnit(seed,branch+450)*2.2,segments:[]},
count=3+Math.floor(reactionUnit(seed,branch+80)*3);
for(let segment=0;segment<count;segment++){angle+=(reactionUnit(seed,branch*11+segment+120)-.5)*.55;const length=24+reactionUnit(seed,branch*13+segment+240)*35;item.segments.push({dx:Math.cos(angle)*length,dy:Math.sin(angle)*length*.68})}
branches.push(item);
}
return branches;
}
function preparedLaserRays(seed){
const rays=[];
for(let index=0;index<30;index++){
const originAngle=reactionUnit(seed,index)*Math.PI*2,targetAngle=originAngle+(.55+reactionUnit(seed,index+100)*1.9)*(index%2?1:-1);
const length=170+reactionUnit(seed,index+150)*260,color=REACTION_LASER_COLORS[index%REACTION_LASER_COLORS.length],strong=index%5===0;
rays.push({originCos:Math.cos(originAngle),originSin:Math.sin(originAngle),targetCos:Math.cos(targetAngle),targetSin:Math.sin(targetAngle),originRadius:18+reactionUnit(seed,index+50)*135,length,speed:index%2?.7:-.45,color,strong});
}
return rays;
}
function preparedOrbitModel(seed){
const stars=[];
for(let index=0;index<32;index++){const angle=reactionUnit(seed,index)*Math.PI*2;stars.push({cos:Math.cos(angle),sin:Math.sin(angle),radius:110+reactionUnit(seed,index+90)*155,size:1+reactionUnit(seed,index+180)*2.2})}
const orbiters=[];
for(let index=0;index<15;index++){const angle=index*Math.PI*2/15;orbiters.push({angle,cos:Math.cos(angle),sin:Math.sin(angle),radius:125+(index%3)*20,size:24+(index%4)*3})}
return{stars,orbiters};
}
function preparedFireworkBloom(seed){
const rings=[[12,72,28],[18,118,24],[24,168,21]].map(([count,maxRadius,size],ring)=>{
const particles=[];for(let index=0;index<count;index++){const angle=index*Math.PI*2/count+(reactionUnit(seed,ring*40+index)-.5)*.08;particles.push({angle,cos:Math.cos(angle),sin:Math.sin(angle),index})}
return{count,maxRadius,size,particles};
});
return{rings,burst:preparedReactionBurst(seed,28)};
}
function prepareReactionModel(reaction){
const started=perfStart(),seed=reactionSeed(reaction.id),style=reaction.style||'classic',model={seed,style,duration:reaction.expiresAt-reaction.createdAt};
if(style==='giant')model.cracks=preparedReactionCracks(seed);
else if(style==='laser')model.rays=preparedLaserRays(seed);
else if(style==='orbit')Object.assign(model,preparedOrbitModel(seed));
else if(style==='firework')model.blooms=[preparedFireworkBloom(seed),preparedFireworkBloom(seed+17),preparedFireworkBloom(seed+31)];
else if(style==='comet'){
model.direction=reactionUnit(seed,2)>.5?1:-1;model.startYJitter=(reactionUnit(seed,3)-.5)*120;model.burst=preparedReactionBurst(seed+99,42);model.trail=[];
for(let index=18;index>=1;index--)model.trail.push({index,delay:index*.032,size:24+(18-index)*.55,alphaFactor:(1-index/20)*.62,scale:.55+index*.012});
model.explosion=[];for(let index=0;index<28;index++){const angle=reactionUnit(seed,index+500)*Math.PI*2;model.explosion.push({angle,cos:Math.cos(angle),sin:Math.sin(angle),radiusFactor:36+reactionUnit(seed,index+560)*235,size:19+(index%5)*3})}
}
perfCount('reactionPreparedModels');perfEnd('reactionPrepare',started);return model;
}
function normalizeRealtimeReaction(raw){
const emoji=String(raw?.emoji||''),x=Number(raw?.x),y=Number(raw?.y),createdAt=Number(raw?.createdAt),expiresAt=Number(raw?.expiresAt),id=typeof raw?.id==='string'?raw.id.slice(0,80):'',style=REACTION_STYLE_IDS.has(raw?.style)?raw.style:'classic';
if(!id||!REACTION_EMOJIS.includes(emoji)||!Number.isFinite(x)||!Number.isFinite(y)||!Number.isFinite(createdAt)||!Number.isFinite(expiresAt)||expiresAt<=trustedNow())return null;
const reaction={id,emoji,style,x,y,createdAt,expiresAt,playerId:raw.playerId||null,playerName:String(raw.playerName||'').slice(0,24)};reaction.prepared=prepareReactionModel(reaction);return reaction;
}
function applyRealtimeReaction(raw){
const started=perfStart(),reaction=normalizeRealtimeReaction(raw);if(!reaction)return false;
realtimeReactions.set(reaction.id,reaction);reactionDirty=true;perfGauge('activeReactions',realtimeReactions.size);scheduleReactionRender();perfEnd('reactionPublish',started);return true;
}
function cancelReactionRenderScheduler(resetDeadline=false){
if(reactionDelayTimer)clearTimeout(reactionDelayTimer);if(reactionWatchdogTimer)clearTimeout(reactionWatchdogTimer);if(reactionFrame)cancelAnimationFrame(reactionFrame);if(reactionCalibrationFrame)cancelAnimationFrame(reactionCalibrationFrame);reactionDelayTimer=0;reactionWatchdogTimer=0;reactionFrame=0;reactionCalibrationFrame=0;if(resetDeadline){reactionNextDrawAt=0;reactionLastDraw=0;reactionLastMetricFrame=0}
}
function commitReactionRender(timestamp,source='raf'){
if(source==='raf'&&reactionNextDrawAt&&timestamp+1<reactionNextDrawAt){scheduleReactionRender();return false}
const previousDraw=reactionLastDraw;if(reactionLastMetricFrame)perfObserve('reactionFrameGap',Math.max(0,timestamp-reactionLastMetricFrame));reactionLastMetricFrame=timestamp;reactionLastDraw=timestamp;
if(!previousDraw){
reactionNextDrawAt=timestamp+REACTION_FRAME_INTERVAL;
if(!reactionVsyncCalibrated)reactionCalibrationFrame=requestAnimationFrame(calibrationTimestamp=>{reactionCalibrationFrame=0;reactionVsyncInterval=Math.max(4,Math.min(20,calibrationTimestamp-timestamp));reactionVsyncCalibrated=true;perfCount('reactionCalibrationRafCallbacks');scheduleReactionRender()});
}else{let advances=0;do{reactionNextDrawAt+=REACTION_FRAME_INTERVAL;advances++}while(reactionNextDrawAt<=timestamp+1);if(advances>1)perfCount('reactionSkippedDeadlines',advances-1)}
markVisualFrame(timestamp);drawReactionLayer(timestamp);return true;
}
function scheduleReactionRender(markDirty=false){
if(markDirty)reactionDirty=true;
if(!reactionCanvas||document.visibilityState==='hidden'||reactionFrame||reactionDelayTimer||reactionCalibrationFrame||(!reactionDirty&&!realtimeReactions.size))return;
const now=perfNow();if(!reactionNextDrawAt)reactionNextDrawAt=now;const wait=reactionNextDrawAt-now;
const timerDelay=wait-reactionVsyncInterval+1.5;
if(timerDelay>2){
reactionDelayTimer=setTimeout(()=>{reactionDelayTimer=0;perfCount('reactionDeadlineTimerCallbacks');scheduleReactionRender()},timerDelay);return;
}
reactionFrame=requestAnimationFrame(timestamp=>{reactionFrame=0;if(reactionWatchdogTimer)clearTimeout(reactionWatchdogTimer);reactionWatchdogTimer=0;perfCount('reactionDrawRafCallbacks');if(document.visibilityState==='hidden')return;commitReactionRender(timestamp,'raf')});
if(reactionLastDraw){
const watchdogDelay=Math.max(2,reactionNextDrawAt-perfNow()+3);
reactionWatchdogTimer=setTimeout(()=>{reactionWatchdogTimer=0;if(!reactionFrame)return;cancelAnimationFrame(reactionFrame);reactionFrame=0;perfCount('reactionWatchdogCallbacks');if(document.visibilityState!=='hidden')commitReactionRender(perfNow(),'watchdog')},watchdogDelay);
}
}
function drawReactionEmoji(context,emoji,x,y,size,alpha=1,rotation=0,scale=1){
perfCount('reactionEmojiDraws');context.save();context.globalAlpha*=Math.max(0,alpha);context.translate(x,y);context.rotate(rotation);context.scale(scale,scale);
context.shadowColor='rgba(0,0,0,.7)';context.shadowBlur=Math.max(4,size*.14);const sprite=rotation===0&&scale===1?reactionGlyphSprite(context,emoji,size):null;
if(sprite)context.drawImage(sprite.canvas,-sprite.width/2,-sprite.height/2);
else{context.font=`${size}px ${REACTION_FONT}`;context.textAlign='center';context.textBaseline='middle';context.fillText(emoji,0,0)}
context.restore();
}
function beginReactionEmojiBatch(context){context.save();context.shadowColor='rgba(0,0,0,.7)';context.textAlign='center';context.textBaseline='middle';return{transform:context.getTransform(),alpha:context.globalAlpha,fontSize:0}}
function drawReactionEmojiBatched(context,batch,emoji,sprite,x,y,size,alpha=1,rotation=0,scale=1){
perfCount('reactionEmojiDraws');context.setTransform(batch.transform);context.globalAlpha=batch.alpha*Math.max(0,alpha);context.translate(x,y);context.rotate(rotation);context.scale(scale,scale);context.shadowBlur=Math.max(4,size*.14);
if(sprite&&rotation===0&&scale===1)context.drawImage(sprite.canvas,-sprite.width/2,-sprite.height/2);
else{if(batch.fontSize!==size){context.font=`${size}px ${REACTION_FONT}`;batch.fontSize=size}context.fillText(emoji,0,0)}
}
function endReactionEmojiBatch(context){context.restore()}
function drawReactionFlash(context,radius,alpha=.8){perfCount('reactionGradients');const gradient=context.createRadialGradient(0,0,0,0,0,radius);gradient.addColorStop(0,`rgba(255,255,255,${alpha})`);gradient.addColorStop(.18,`rgba(255,240,170,${alpha*.75})`);gradient.addColorStop(.55,`rgba(255,90,210,${alpha*.28})`);gradient.addColorStop(1,'rgba(70,190,255,0)');context.save();context.globalCompositeOperation='screen';context.fillStyle=gradient;context.beginPath();context.arc(0,0,radius,0,Math.PI*2);context.fill();context.restore();perfCount('reactionPaths')}
function drawReactionBurst(context,rays,progress,maxRadius=120,alpha=1){const eased=1-Math.pow(1-Math.max(0,Math.min(1,progress)),3);context.save();context.globalCompositeOperation='screen';context.lineCap='round';for(const ray of rays){const radius=maxRadius*ray.radiusFactor*eased,inner=Math.max(4,radius-ray.innerOffset);context.strokeStyle=`hsla(${ray.hue},100%,72%,${alpha*(1-progress)})`;context.lineWidth=ray.lineWidth;context.beginPath();context.moveTo(ray.cos*inner,ray.sin*inner);context.lineTo(ray.cos*radius,ray.sin*radius);context.stroke()}context.restore();perfCount('reactionPaths',rays.length)}
function drawCrackedGround(context,branches,progress){const reveal=Math.min(1,progress*4),alpha=(1-progress*.55)*reveal;context.save();context.globalCompositeOperation='screen';context.lineCap='round';context.shadowColor='rgba(140,220,255,.75)';context.shadowBlur=7;for(const branch of branches){let x=branch.x,y=branch.y;context.beginPath();context.moveTo(x,y);for(const segment of branch.segments){x+=segment.dx*reveal;y+=segment.dy*reveal;context.lineTo(x,y)}context.strokeStyle=`rgba(205,245,255,${alpha*branch.alpha})`;context.lineWidth=branch.lineWidth;context.stroke()}context.restore();perfCount('reactionPaths',branches.length)}
function drawGiantReaction(context,emoji,life,model){const impact=Math.min(1,life/.2),impactEase=1-Math.pow(1-impact,4),after=Math.max(0,(life-.16)/.84),shake=(1-after)*Math.sin(after*96)*13;context.save();context.translate(Math.sin(after*137)*shake,Math.cos(after*113)*shake*.62);drawCrackedGround(context,model.cracks,after);if(life>.12)drawReactionFlash(context,190*(1-after)+70,Math.max(0,.72-after*.8));for(let ring=0;ring<3;ring++){const p=Math.max(0,Math.min(1,(after-ring*.07)*1.45));context.save();context.globalCompositeOperation='screen';context.strokeStyle=`rgba(${ring===1?'255,95,220':'115,225,255'},${(1-p)*.62})`;context.lineWidth=8-2*ring;context.beginPath();context.ellipse(0,24,30+230*p,16+105*p,0,0,Math.PI*2);context.stroke();context.restore()}perfCount('reactionPaths',3);const squash=1+Math.sin(Math.min(1,impact)*Math.PI)*.2;drawReactionEmoji(context,emoji,0,38-78*impactEase,142,1,0,(.18+.92*impactEase)*squash);context.restore()}
function drawLaserReaction(context,emoji,life,model){const attack=Math.min(1,life/.1),fade=Math.max(0,1-Math.max(0,life-.78)/.22),spin=life*Math.PI*7,positiveAngle=spin*.7,negativeAngle=spin*-.45,positiveCos=Math.cos(positiveAngle),positiveSin=Math.sin(positiveAngle),negativeCos=Math.cos(negativeAngle),negativeSin=Math.sin(negativeAngle);drawReactionFlash(context,90+25*Math.sin(life*22),.34*fade);context.save();context.globalCompositeOperation='screen';context.lineCap='round';for(const ray of model.rays){const ct=ray.speed>0?positiveCos:negativeCos,st=ray.speed>0?positiveSin:negativeSin,originCos=ray.originCos*ct-ray.originSin*st,originSin=ray.originSin*ct+ray.originCos*st,targetCos=ray.targetCos*ct-ray.targetSin*st,targetSin=ray.targetSin*ct+ray.targetCos*st,x1=originCos*ray.originRadius,y1=originSin*ray.originRadius*.72,x2=x1+targetCos*ray.length,y2=y1+targetSin*ray.length,gradient=context.createLinearGradient(x1,y1,x2,y2);perfCount('reactionGradients');gradient.addColorStop(0,'rgba(255,255,255,.92)');gradient.addColorStop(.12,ray.color);gradient.addColorStop(1,'rgba(80,220,255,0)');context.strokeStyle=gradient;context.lineWidth=(ray.strong?5:2.2)*attack*fade;context.shadowColor=ray.color;context.shadowBlur=ray.strong?16:8;context.beginPath();context.moveTo(x1,y1);context.lineTo(x2,y2);context.stroke()}for(let index=0;index<7;index++){const angle=spin*.6+index*Math.PI*2/7,radius=62+24*Math.sin(life*18+index);context.fillStyle=REACTION_LASER_COLORS[index%REACTION_LASER_COLORS.length];context.beginPath();context.arc(Math.cos(angle)*radius,Math.sin(angle)*radius*.68,3+3*Math.sin(life*24+index)**2,0,Math.PI*2);context.fill()}perfCount('reactionPaths',37);context.restore();drawReactionEmoji(context,emoji,0,0,60,fade,spin*3.8,.65+.4*attack)}
function drawOrbitReaction(context,emoji,life,model){
const enter=Math.min(1,life/.16),fade=Math.max(0,1-Math.max(0,life-.84)/.16),scale=.35+.65*(1-Math.pow(1-enter,4)),spin=life*Math.PI*3.6,ct=Math.cos(spin),st=Math.sin(spin);context.save();context.scale(scale,scale);perfCount('reactionGradients');
const nebula=context.createRadialGradient(0,0,18,0,0,250);nebula.addColorStop(0,`rgba(95,175,255,${.24*fade})`);nebula.addColorStop(.48,`rgba(115,70,220,${.14*fade})`);nebula.addColorStop(1,'rgba(20,10,80,0)');context.fillStyle=nebula;const nebulaPath=reactionOrbitNebulaPath();if(nebulaPath)context.fill(nebulaPath);else{context.beginPath();context.arc(0,0,250,0,Math.PI*2);context.fill()}
context.save();context.rotate(-.34);context.globalCompositeOperation='screen';for(let ring=0;ring<5;ring++){context.strokeStyle=ring%2?`rgba(255,210,120,${(.5-ring*.05)*fade})`:`rgba(125,220,255,${(.68-ring*.07)*fade})`;context.lineWidth=ring===2?8:2.5+ring*.7;const ringPath=reactionOrbitRingPath(ring);if(ringPath)context.stroke(ringPath);else{context.beginPath();context.ellipse(0,0,118+ring*15,37+ring*8,0,0,Math.PI*2);context.stroke()}}
const batch=beginReactionEmojiBatch(context);for(const orbiter of model.orbiters){const cos=orbiter.cos*ct-orbiter.sin*st,sin=orbiter.sin*ct+orbiter.cos*st;drawReactionEmojiBatched(context,batch,emoji,null,cos*orbiter.radius,sin*orbiter.radius*.34,orbiter.size,.72*fade,-.15,.78)}endReactionEmojiBatch(context);context.restore();
for(let index=0;index<model.stars.length;index++){const star=model.stars[index],twinkle=.25+.75*Math.sin(life*26+index)**2;context.fillStyle=`rgba(220,245,255,${twinkle*fade})`;context.beginPath();context.arc(star.cos*star.radius,star.sin*star.radius,star.size,0,Math.PI*2);context.fill()}perfCount('reactionPaths',38);drawReactionEmoji(context,emoji,0,0,112,fade,Math.sin(life*4)*.025,1);context.restore();
}
function drawFireworkBloom(context,emoji,model,progress,x=0,y=0,scale=1){if(progress<=0||progress>=1)return;const open=1-Math.pow(1-Math.min(1,progress*1.18),3),fade=Math.max(0,1-Math.max(0,progress-.72)/.28);context.save();context.translate(x,y);context.scale(scale,scale);drawReactionFlash(context,48+85*open,.55*fade);const batch=beginReactionEmojiBatch(context);for(let ringIndex=0;ringIndex<model.rings.length;ringIndex++){const ring=model.rings[ringIndex],ringProgress=Math.max(0,Math.min(1,(progress-ringIndex*.055)*1.18)),radius=ring.maxRadius*(1-Math.pow(1-ringProgress,3));for(const particle of ring.particles){const alpha=fade*(.76+.24*Math.sin(particle.index+progress*18)**2),particleScale=.7+.3*ringProgress;drawReactionEmojiBatched(context,batch,emoji,null,particle.cos*radius,particle.sin*radius,ring.size,alpha,particle.angle,particleScale)}}endReactionEmojiBatch(context);drawReactionBurst(context,model.burst,progress,195,.85*fade);context.restore()}
function drawFireworkReaction(context,emoji,life,model){
const launch=Math.min(1,life/.3),launchEase=1-Math.pow(1-launch,3);if(life<.34){context.save();context.globalCompositeOperation='screen';const y=190-190*launchEase;context.translate(0,y);for(let index=0;index<12;index++){const p=index/12;context.fillStyle=`rgba(255,${150+Math.round(p*100)},90,${(1-p)*.75})`;const launchPath=reactionFireworkLaunchPath(index);if(launchPath)context.fill(launchPath);else{context.beginPath();context.arc(Math.sin(index*2.1)*5,index*12,2.5+(1-p)*2.5,0,Math.PI*2);context.fill()}}perfCount('reactionPaths',12);context.restore();drawReactionEmoji(context,emoji,0,y,38,1,-.08,.8+.25*launchEase)}
const bloom=Math.max(0,(life-.24)/.76);drawFireworkBloom(context,emoji,model.blooms[0],bloom,0,0,1);drawFireworkBloom(context,emoji,model.blooms[1],Math.max(0,bloom-.14)/.86,-165,42,.58);drawFireworkBloom(context,emoji,model.blooms[2],Math.max(0,bloom-.24)/.76,170,-24,.48);
}
function drawCometReaction(context,emoji,life,model){const travel=Math.min(1,life/.7),travelEase=1-Math.pow(1-travel,4),explode=Math.max(0,(life-.68)/.32),width=context.canvas?.clientWidth||context.canvas?.width||900,height=context.canvas?.clientHeight||context.canvas?.height||600,startX=-model.direction*Math.max(420,width*.62),startY=-Math.max(290,height*.48)+model.startYJitter,x=startX*(1-travelEase),y=startY*(1-travelEase),alpha=Math.max(0,1-life);context.save();if(explode>0){const shake=(1-explode)*22;context.translate(Math.sin(explode*165)*shake,Math.cos(explode*131)*shake)}if(travel<1){const angle=Math.atan2(-startY,-startX);context.save();context.globalCompositeOperation='screen';const batch=beginReactionEmojiBatch(context);for(const trail of model.trail){const p=Math.max(0,Math.min(1,(travel-trail.delay)/(1-trail.delay))),e=1-Math.pow(1-p,4);drawReactionEmojiBatched(context,batch,emoji,null,startX*(1-e),startY*(1-e),trail.size,alpha*trail.alphaFactor,angle,trail.scale)}endReactionEmojiBatch(context);perfCount('reactionGradients');const gradient=context.createLinearGradient(x,y,startX*(1-Math.max(0,travelEase-.32)),startY*(1-Math.max(0,travelEase-.32)));gradient.addColorStop(0,`rgba(255,255,255,${.95*alpha})`);gradient.addColorStop(.18,`rgba(255,215,80,${.8*alpha})`);gradient.addColorStop(.48,`rgba(255,75,210,${.45*alpha})`);gradient.addColorStop(1,'rgba(70,190,255,0)');context.strokeStyle=gradient;context.lineWidth=18*(1-travel)+5;context.lineCap='round';context.beginPath();context.moveTo(x,y);context.lineTo(x-Math.cos(angle)*(120+220*(1-travel)),y-Math.sin(angle)*(120+220*(1-travel)));context.stroke();perfCount('reactionPaths');context.restore();drawReactionEmoji(context,emoji,x,y,52,alpha,angle,.72+.48*travelEase)}if(explode>0){drawReactionFlash(context,80+210*(1-Math.pow(1-explode,3)),.95*(1-explode));for(let ring=0;ring<3;ring++){const p=Math.max(0,Math.min(1,(explode-ring*.07)*1.22));context.save();context.globalCompositeOperation='screen';context.strokeStyle=`rgba(${ring===1?'255,90,220':'120,230,255'},${(1-p)*.82})`;context.lineWidth=10-ring*2.4;context.beginPath();context.arc(0,0,24+250*p,0,Math.PI*2);context.stroke();context.restore()}perfCount('reactionPaths',3);drawReactionBurst(context,model.burst,explode,280,1);const eased=1-Math.pow(1-explode,3),batch=beginReactionEmojiBatch(context);for(const particle of model.explosion)drawReactionEmojiBatched(context,batch,emoji,null,particle.cos*particle.radiusFactor*eased,particle.sin*particle.radiusFactor*eased,particle.size,(1-explode)*.86,particle.angle,.62);endReactionEmojiBatch(context)}context.restore()}
function drawStyledReaction(context,reaction,life,fade){
const emoji=reaction.emoji,style=reaction.style||'classic',model=reaction.prepared||(reaction.prepared=prepareReactionModel(reaction)),started=perfStart();
context.save();context.globalAlpha=Math.max(0,fade);
if(style==='giant')drawGiantReaction(context,emoji,life,model);
else if(style==='laser')drawLaserReaction(context,emoji,life,model);
else if(style==='orbit')drawOrbitReaction(context,emoji,life,model);
else if(style==='firework')drawFireworkReaction(context,emoji,life,model);
else if(style==='comet')drawCometReaction(context,emoji,life,model);
else{const enter=Math.min(1,life/.16),out=Math.max(0,1-Math.max(0,life-.48)/.52),floatY=-18*(1-Math.pow(1-life,1.7));drawReactionEmoji(context,emoji,0,floatY,40,out,0,.72+.28*(1-Math.pow(1-enter,3)))}
context.restore();perfEnd(`reactionStyle.${style}`,started);
}
function drawReactionLayer(){
if(!reactionCanvas)return;const started=perfStart(),rect=getViewportRect(),width=Math.max(1,Math.round(rect.width)),height=Math.max(1,Math.round(rect.height)),dpr=1,pixelWidth=Math.round(width*dpr),pixelHeight=Math.round(height*dpr);
if(reactionCanvas.width!==pixelWidth||reactionCanvas.height!==pixelHeight){reactionCanvas.width=pixelWidth;reactionCanvas.height=pixelHeight}
reactionCanvas.style.transform='none';reactionCameraSnapshot=onlineLayerCameraSnapshot();const context=reactionCanvas.getContext('2d',{alpha:true}),compositeStarted=perfStart();context.setTransform(dpr,0,0,dpr,0,0);context.clearRect(0,0,width,height);perfCount('reactionCanvasPixelsCleared',width*height);reactionDirty=false;
const timestamp=trustedNow();let active=0,visible=0;
for(const[id,reaction]of realtimeReactions){
if(reaction.expiresAt<=timestamp){realtimeReactions.delete(id);perfCount('reactionModelsReleased');continue}
active++;const life=Math.max(0,Math.min(1,(timestamp-reaction.createdAt)/(reaction.expiresAt-reaction.createdAt))),fade=life>.94?(1-life)/.06:1,x=(reaction.x-renderOriginX)*UNIT*cam.scale+cam.x,y=(reaction.y-renderOriginY)*UNIT*cam.scale+cam.y;
if(x<-420||y<-420||x>width+420||y>height+420)continue;
visible++;context.save();context.translate(x,y);drawStyledReaction(context,reaction,life,fade);context.restore();
}
perfEnd('reactionComposite',compositeStarted);perfGauge('activeReactions',active);perfGauge('visibleReactions',visible);perfGauge('peakVisibleReactions',Math.max(perfGauges.peakVisibleReactions||0,visible));perfGauge('preparedReactionModels',realtimeReactions.size);perfGauge('reactionOverload',active>8?active:0);perfCount('reactionFrames');perfEnd('reactionFrame',started);
if(active)scheduleReactionRender();else reactionNextDrawAt=0;
}
function renderReactionSample({style='classic',emoji='😀',life=.5,width=900,height=700,glyphCache=true,pathCache=true}={}){
const normalizedStyle=REACTION_STYLE_IDS.has(style)?style:'classic',normalizedEmoji=REACTION_EMOJIS.includes(emoji)?emoji:REACTION_EMOJIS[0],normalizedLife=Math.max(0,Math.min(1,Number(life)||0)),canvas=document.createElement('canvas');canvas.width=Math.max(1,Math.round(width));canvas.height=Math.max(1,Math.round(height));
const context=canvas.getContext('2d',{alpha:true}),reaction={id:`sample-${normalizedStyle}`,emoji:normalizedEmoji,style:normalizedStyle,x:0,y:0,createdAt:0,expiresAt:reactionDurationForStyle(normalizedStyle)};reaction.prepared=prepareReactionModel(reaction);context.translate(canvas.width/2,canvas.height/2);
if(!glyphCache)reactionGlyphCacheBypass++;if(!pathCache)reactionStaticPathCacheBypass++;try{drawStyledReaction(context,reaction,normalizedLife,normalizedLife>.94?(1-normalizedLife)/.06:1)}finally{if(!glyphCache)reactionGlyphCacheBypass--;if(!pathCache)reactionStaticPathCacheBypass--}
return{style:normalizedStyle,life:normalizedLife,width:canvas.width,height:canvas.height,dataUrl:canvas.toDataURL('image/png')};
}
let activeLocalSpecialReactionUntil=0;
function specialReactionInputLocked(){return data?.reactionStyle!=='classic'&&Date.now()<activeLocalSpecialReactionUntil}
function publishReactionAt(emoji,x,y){if(!REACTION_EMOJIS.includes(emoji)||!Number.isFinite(x)||!Number.isFinite(y))return false;const timestamp=Date.now(),style=REACTION_STYLE_IDS.has(data.reactionStyle)?data.reactionStyle:'classic';if(style!=='classic'&&timestamp<activeLocalSpecialReactionUntil){toast('特殊エフェクトの再生中です。');return false}data.lastReaction=emoji;markGlobalDirty(false);const expiresAt=timestamp+reactionDurationForStyle(style),reaction={id:`local-${sessionId}-${++reactionSequence}`,emoji,style,x,y,createdAt:timestamp,expiresAt,playerId:currentPlayerId(),playerName:currentPlayerName()};if(style!=='classic')activeLocalSpecialReactionUntil=expiresAt;applyRealtimeReaction(reaction);sendOrQueueRealtimeReaction(reaction);return true}
function reactionAllowedAt(clientX,clientY,target){if(target?.closest?.('button,a,input,select,textarea,[role="button"]'))return false;const[x,y]=worldUnitAtClient(clientX,clientY),boardId=occupancy.get(key2(Math.floor(x),Math.floor(y)));return!boardId||metaState(boardId).solved}
function hideReactionRadial(){if(reactionRadial)reactionRadial.hidden=true;document.body.classList.remove('reaction-selecting')}
function radialReactionIndex(clientX,clientY){if(!reactionGesture)return 0;const dx=clientX-reactionGesture.clientX,dy=clientY-reactionGesture.clientY,distance=Math.hypot(dx,dy);if(distance<18)return reactionGesture.selected??0;let best=0,bestDistance=Infinity;for(let index=0;index<REACTION_EMOJIS.length;index++){const angle=-Math.PI/2+index*Math.PI*2/REACTION_EMOJIS.length,cx=Math.cos(angle)*48,cy=Math.sin(angle)*48,delta=(dx-cx)**2+(dy-cy)**2;if(delta<bestDistance){best=index;bestDistance=delta}}return best}
function updateReactionRadialSelection(clientX,clientY){if(!reactionGesture?.menuOpen)return;reactionGesture.selected=radialReactionIndex(clientX,clientY);for(const[ index,button]of[...reactionRadial.children].entries())button.classList.toggle('selected',index===reactionGesture.selected)}
function openReactionRadial(){const gesture=reactionGesture;if(!gesture||gesture.moved||!gestureCoordinator.claim(gesture.pointerId,'reaction'))return;gesture.menuOpen=true;interactionState.set('reaction',gesture.pointerId);gesture.selected=REACTION_EMOJIS.indexOf(data.lastReaction);if(gesture.selected<0)gesture.selected=0;if(pan?.id===gesture.pointerId){pan=null;viewport.classList.remove('panning');refreshInteractionState()}reactionRadial.replaceChildren();REACTION_EMOJIS.forEach((emoji,index)=>{const button=document.createElement('button'),angle=-Math.PI/2+index*Math.PI*2/REACTION_EMOJIS.length;button.type='button';button.textContent=emoji;button.setAttribute('role','menuitem');button.style.setProperty('--rx',`${Math.cos(angle)*48}px`);button.style.setProperty('--ry',`${Math.sin(angle)*48}px`);button.classList.toggle('selected',index===gesture.selected);reactionRadial.append(button)});reactionRadial.style.left=`${gesture.clientX}px`;reactionRadial.style.top=`${gesture.clientY}px`;reactionRadial.hidden=false;document.body.classList.add('reaction-selecting')}
function beginReactionGesture(event){if(specialReactionInputLocked())return;if(event.button!==0||event.pointerType==='touch'&&!event.isPrimary||!reactionAllowedAt(event.clientX,event.clientY,event.target))return;const[x,y]=worldUnitAtClient(event.clientX,event.clientY);reactionGesture={pointerId:event.pointerId,clientX:event.clientX,clientY:event.clientY,x,y,moved:false,menuOpen:false,selected:REACTION_EMOJIS.indexOf(data.lastReaction),timer:setTimeout(openReactionRadial,REACTION_LONG_PRESS_MS)};try{viewport.setPointerCapture(event.pointerId)}catch(_){}}
function moveReactionGesture(event){const gesture=reactionGesture;if(!gesture||gesture.pointerId!==event.pointerId)return;if(gesture.menuOpen){event.preventDefault();event.stopImmediatePropagation();updateReactionRadialSelection(event.clientX,event.clientY);return}if(Math.hypot(event.clientX-gesture.clientX,event.clientY-gesture.clientY)>REACTION_MOVE_CANCEL_PX){gesture.moved=true;clearTimeout(gesture.timer)}}
function endReactionGesture(event){const gesture=reactionGesture;if(!gesture||gesture.pointerId!==event.pointerId)return;clearTimeout(gesture.timer);const index=gesture.menuOpen?radialReactionIndex(event.clientX,event.clientY):-1;reactionGesture=null;gestureCoordinator.release(event.pointerId,'reaction');interactionState.clear('reaction');try{if(viewport.hasPointerCapture?.(event.pointerId))viewport.releasePointerCapture(event.pointerId)}catch(_){}if(gesture.menuOpen){event.preventDefault();event.stopImmediatePropagation();touchPoints.delete(event.pointerId);viewport.classList.remove('panning');refreshInteractionState();const emoji=REACTION_EMOJIS[index]||data.lastReaction;hideReactionRadial();publishReactionAt(emoji,gesture.x,gesture.y);return}hideReactionRadial();if(!gesture.moved)publishReactionAt(data.lastReaction||'👍',gesture.x,gesture.y)}
function cancelReactionGesture(event=null){const pointerId=reactionGesture?.pointerId;if(reactionGesture?.timer)clearTimeout(reactionGesture.timer);reactionGesture=null;if(pointerId!=null){gestureCoordinator.release(pointerId,'reaction','cancelled');try{if(viewport.hasPointerCapture?.(pointerId))viewport.releasePointerCapture(pointerId)}catch(_){}}interactionState.clear('reaction');hideReactionRadial()}
function currentBoardClaim(boardId){const claim=boardClaims.get(boardId);if(claim&&claim.expiresAt<=trustedNow()){boardClaims.delete(boardId);refreshClaimPresentation(boardId);return null}return claim||null}
function boardClaimOwnedByMe(boardId){const claim=currentBoardClaim(boardId);return Boolean(claim&&claim.playerId===currentPlayerId())}
function applyClaimPresentationToBoard(board){if(!board)return;const claim=currentBoardClaim(board.id),solved=metaState(board.id).solved,own=Boolean(claim&&claim.playerId===currentPlayerId()),hudVisible=boardPlayHudVisible(board,solved);board.card.classList.toggle('claimed-other',Boolean(claim&&!own&&!solved));board.card.classList.toggle('claimed-own',Boolean(claim&&own&&!solved));setBoardHudVisibility(board,hudVisible);board.label?.classList.toggle('claimed-own',Boolean(claim&&own&&!solved));if(board.claimBadge){board.claimBadge.hidden=!claim||solved;board.claimBadge.textContent=claim?own?'プレイ中':`${claim.playerName||'他のプレイヤー'}がプレイ中`:''}board.svg?.setAttribute?.('aria-disabled',String(Boolean(claim&&!own&&!solved)))}
function refreshClaimPresentation(boardId=null){
const ids=boardId?[boardId]:rendered.keys();for(const id of ids){const board=rendered.get(id);if(board)applyClaimPresentationToBoard(board)}
}
function applyClaim(raw){if(!raw?.boardId)return;if(metaState(raw.boardId)?.solved===true){removeClaim(raw.boardId,'cleared');return}boardClaims.set(raw.boardId,{...raw,expiresAt:Number(raw.expiresAt)||trustedNow()});if(raw.playerId===currentPlayerId())realtimeOwnClaimBoardId=raw.boardId;refreshClaimPresentation(raw.boardId)}
function preserveActiveDrawingClaim(boardId,claim,reason){
const board=rendered.get(boardId),pointerId=board?.drawing?.pointerId;if(reason==='cleared'||claim?.playerId!==currentPlayerId()||pointerId==null||!realtimeHeldPointers.has(pointerId))return false;
claim.expiresAt=Math.max(Number(claim.expiresAt)||0,trustedNow()+30000);boardClaims.set(boardId,claim);realtimeOwnClaimBoardId=boardId;refreshClaimPresentation(boardId);
void requestBoardClaim(boardId,{force:true}).then(ok=>{if(!ok&&board?.drawing?.pointerId===pointerId)console.warn(`LinkField: board claim recovery is pending for ${boardId}`)});return true;
}
function removeClaim(boardId,reason='released'){const claim=boardClaims.get(boardId);if(preserveActiveDrawingClaim(boardId,claim,reason))return false;boardClaims.delete(boardId);if(realtimeOwnClaimBoardId===boardId)realtimeOwnClaimBoardId=null;refreshClaimPresentation(boardId);return true}
function applyRealtimeSnapshot(message){
const seenPlayers=new Set();for(const player of message.players||[]){seenPlayers.add(player.presenceId);applyRemotePlayer(player)}for(const id of[...remotePlayers.keys()])if(!seenPlayers.has(id))remotePlayers.delete(id);
const seenClaims=new Set();for(const claim of message.claims||[]){seenClaims.add(claim.boardId);applyClaim(claim)}for(const id of[...boardClaims.keys()])if(!seenClaims.has(id))removeClaim(id,'viewport');for(const reaction of message.reactions||[])applyRealtimeReaction(reaction);schedulePresenceRender(true);refreshClaimPresentation();
}
function applyRealtimeWorldDelta(message){
const revision=Math.max(0,Number(message?.revision)||0),page=message?.page;if(!revision||revision<=(data.cloudRevision||0)||!page||!isPlainObject(page.metas)||!isPlainObject(page.states))return false;
const external={...(page.global||{}),worldEpoch:data.worldEpoch,metas:{},states:{}};
for(const[id,raw]of Object.entries(page.metas)){const meta=normalizeMeta(id,raw);if(meta)external.metas[id]=meta}
for(const[id,raw]of Object.entries(page.states)){const board=rendered.get(id),locallyOwned=boardClaimOwnedByMe(id)||board?.drawing?.pointerId!=null||board?.drawing?.keyboardActive;if(!locallyOwned)external.states[id]=normalizeState(raw)}
cloudApplyingRemote=true;try{mergeSnapshotIntoData(external,{finalize:false,authoritativeWorld:false});for(const id of Object.keys(external.metas))dirtyMetaIds.add(id);for(const id of Object.keys(external.states))dirtyStateIds.add(id);resolveMergedOverlaps();data.cloudRevision=revision;remoteWorldRevision=Math.max(remoteWorldRevision,revision);statsDirty=true;markGlobalDirty(false)}finally{cloudApplyingRemote=false}
void refreshWorldView({rebuild:true,syncConnections:true,persist:false});void persistNow({skipCloud:true}).then(ok=>{if(!ok)scheduleCloudCheckpointRetry()});return true;
}
function settleRealtimeClaimResult(message){const pending=realtimeClaimRequests.get(message.requestId);if(!pending)return;if(message.claim)applyClaim(message.claim);pending.resolve(message)}
function handleRealtimeMessage(message){
if(!message||typeof message!=='object')return;
if(message.type==='ready'){realtimeReady=true;realtimePresenceId=message.presenceId;if(realtimeTransport==='websocket')realtimeWebSocketFailures=0;setCloudStatus();realtimeClaimTtlMs=Math.max(1000,Number(message.claimTtlMs)||realtimeClaimTtlMs);serverClockOffset=Number.isFinite(message.serverTime)?message.serverTime-Date.now():serverClockOffset;scheduleRealtimeViewport(true);if(realtimePendingCursor)realtimeSend({type:'cursor',...realtimePendingCursor});flushPendingRealtimeReactions();scheduleRealtimeHeartbeat();if(realtimeOwnClaimBoardId&&rendered.get(realtimeOwnClaimBoardId)?.drawing?.pointerId!=null)void requestBoardClaim(realtimeOwnClaimBoardId,{force:true});return}
if(message.type==='snapshot')return applyRealtimeSnapshot(message);
if(message.type==='cursor')return applyRemotePlayer(message);
if(message.type==='player-left'){remotePlayers.delete(message.presenceId);schedulePresenceRender(true);scheduleMinimap(true);return}
if(message.type==='claim')return applyClaim(message.claim);
if(message.type==='claim-release'){removeClaim(message.boardId,message.reason);if(message.reason!=='moved')void pullCloudWorld();return}
if(message.type==='claim-result')return settleRealtimeClaimResult(message);
if(message.type==='reaction')return applyRealtimeReaction(message.reaction);
if(message.type==='player-profile'){for(const player of remotePlayers.values())if(player.playerId===message.playerId)player.name=String(message.name||player.name).slice(0,24);for(const claim of boardClaims.values())if(claim.playerId===message.playerId)claim.playerName=String(message.name||claim.playerName).slice(0,24);refreshClaimPresentation();schedulePresenceRender(true);return}
if(message.type==='board-cleared'&&message.event){const revision=Number(message.event.revision)||0;if(revision>(data.worldFeedRevision||0)){data.worldFeedRevision=revision;markGlobalDirty(false);addClearFeedEvent(message.event)}removeClaim(message.event.id,'cleared');void pullCloudWorld();}
if(message.type==='world-revision'){if(!applyRealtimeWorldDelta(message))scheduleRemoteWorldPull(Number(message.revision)||0);return}
}
function rejectRealtimeClaims(){for(const pending of realtimeClaimRequests.values()){clearTimeout(pending.timer);pending.resolve(false)}realtimeClaimRequests.clear()}
function scheduleRealtimeReconnect(delay=1500){clearTimeout(realtimeReconnectTimer);if(!cloudAvailable||!data.cloudProfile||document.visibilityState==='hidden')return;realtimeReconnectTimer=setTimeout(connectRealtime,delay)}
function resetRealtimeConnectionState({clearRemote=true}={}){clearTimeout(realtimeHeartbeatTimer);realtimeHeartbeatTimer=0;clearTimeout(realtimePollTimer);realtimePollTimer=0;realtimeReady=false;realtimePresenceId=null;realtimeSocket=null;realtimeTransport='none';setCloudStatus();if(clearRemote){remotePlayers.clear();realtimeReactions.clear();reactionDirty=true;rejectRealtimeClaims();schedulePresenceRender(true);scheduleReactionRender(true);scheduleMinimap(true)}}
function connectRealtimeHttp(){
if(realtimeHttpConnecting||realtimeTransport==='http-poll'&&realtimePresenceId)return true;realtimeHttpConnecting=true;realtimeTransport='http-poll';realtimeReady=false;realtimePollSequence=0;
void enqueueRealtimeHttp(async()=>{try{const result=await fetchJson('/api/realtime/connect',{method:'POST',headers:cloudAuthHeaders(),body:'{}'},5000);applyRealtimeHttpEnvelope(result);scheduleRealtimeHttpPoll(100)}catch(error){resetRealtimeConnectionState();scheduleRealtimeReconnect(1500)}finally{realtimeHttpConnecting=false}});return true;
}
function connectRealtime(){
clearTimeout(realtimeReconnectTimer);realtimeReconnectTimer=0;if(!cloudApiEnabled||!cloudAvailable||!data.cloudProfile)return false;
if(cloudApiUsesPhpBridge()||runtimeConfig.realtimeTransport==='http-poll'||typeof WebSocket==='undefined'||realtimeWebSocketFailures>=2)return connectRealtimeHttp();
if(realtimeSocket&&[WebSocket.OPEN,WebSocket.CONNECTING].includes(realtimeSocket.readyState))return true;
const socket=new WebSocket(realtimeWebSocketUrl());realtimeSocket=socket;realtimeTransport='websocket';realtimeReady=false;
socket.onopen=()=>{if(socket!==realtimeSocket)return;socket.send(JSON.stringify({type:'hello',playerId:data.cloudProfile.playerId,token:data.cloudProfile.token}))};
socket.onmessage=event=>{if(socket!==realtimeSocket)return;try{handleRealtimeMessage(JSON.parse(event.data))}catch(error){console.warn('BEND FIELD: realtime message failed',error)}};
socket.onerror=()=>{};socket.onclose=()=>{if(socket!==realtimeSocket)return;realtimeWebSocketFailures++;resetRealtimeConnectionState();scheduleRealtimeReconnect(realtimeWebSocketFailures>=2?100:1500)};return true;
}
const directBoardClaimRequests=new Map();
function requestBoardClaimThroughRealtime(boardId,timeout=2200){
if(!realtimeReady)return Promise.resolve(null);
const requestId=`claim-${sessionId}-${++realtimeRequestSequence}`;
return new Promise(resolve=>{
const finish=result=>{const pending=realtimeClaimRequests.get(requestId);if(!pending)return;if(pending.timer)clearTimeout(pending.timer);realtimeClaimRequests.delete(requestId);resolve(result)};
const timer=setTimeout(()=>finish(null),timeout);realtimeClaimRequests.set(requestId,{timer,resolve:message=>finish(message)});
if(!realtimeSend({type:'claim',requestId,boardId}))finish(null);
});
}
async function requestBoardClaim(boardId,{allowLocalSolved=false,force=false}={}){
if(!allowLocalSolved&&metaState(boardId)?.solved||!cloudAvailable||!data.cloudProfile)return false;
const existing=currentBoardClaim(boardId),now=trustedNow();
if(existing?.playerId!==currentPlayerId()&&existing?.expiresAt>now)return false;
if(!force&&existing?.playerId===currentPlayerId()&&existing.expiresAt-now>30000)return true;
if(directBoardClaimRequests.has(boardId))return directBoardClaimRequests.get(boardId);
const request=(async()=>{
try{
try{
const result=await fetchJson('/api/realtime/claim',{method:'POST',headers:cloudAuthHeaders(),body:JSON.stringify({boardId,presenceId:realtimePresenceId})},2500);
if(result?.claim)applyClaim(result.claim);
if(result?.ok===true)return true;
if(result?.ok===false)return false;
}catch(error){console.warn('LinkField: direct board claim unavailable; using realtime transport',error)}
const realtimeResult=await requestBoardClaimThroughRealtime(boardId,3000);
if(realtimeResult?.claim)applyClaim(realtimeResult.claim);
return realtimeResult?.ok===true;
}catch(error){console.warn('LinkField: board claim failed',error);return false}
finally{directBoardClaimRequests.delete(boardId)}
})();
directBoardClaimRequests.set(boardId,request);return request;
}
async function ensureBoardClaimForInput(board){if(!board||metaState(board.id).solved)return false;return requestBoardClaim(board.id)}
function touchBoardClaim(boardId,force=false){if(!boardClaimOwnedByMe(boardId)||!realtimeReady)return false;const now=Date.now();if(!force&&now-realtimeLastClaimTouchAt<REALTIME_CLAIM_TOUCH_INTERVAL)return true;realtimeLastClaimTouchAt=now;const sent=realtimeSend({type:'claim-touch',boardId});if(sent){const claim=boardClaims.get(boardId);if(claim)claim.expiresAt=trustedNow()+realtimeClaimTtlMs}return sent}
window.addEventListener('pointerdown',event=>realtimeHeldPointers.add(event.pointerId),{passive:true,capture:true});
for(const type of['pointerup','pointercancel'])window.addEventListener(type,event=>realtimeHeldPointers.delete(event.pointerId),{passive:true,capture:true});
window.addEventListener('pointermove',queueRealtimeCursor,{passive:true,capture:true});
const SHARED_PERSONAL_FIELDS=Object.freeze(['cloudProfile','playerName','playerPurchases','playerEarnedScore','starterLineColor','lineColorStyle','lineEffectStyle','reactionStyle','lastReaction','timeAttack','timeAttackRev','timeAttackCooldowns','lastTimeAttack','timeAttackSuggestionsDisabled','cursorStyle','scoreLensEnabled']);
async function activateSingleSharedClientCache(){
if(!idbAvailable)return true;
const epoch=data?.worldEpoch;if(!validWorldEpoch(epoch))throw new Error('The shared client cache epoch is invalid.');
const db=await openWorldDb(),updatedAt=trustedNow(),tx=db.transaction(['control','worlds'],'readwrite'),done=transactionDone(tx),controlStore=tx.objectStore('control'),worldsStore=tx.objectStore('worlds'),
current=await requestValue(controlStore.get('active'));
if(current?.activeEpoch&&current.activeEpoch!==epoch){const previous=await requestValue(worldsStore.get(current.activeEpoch));if(previous){previous.status='garbage';worldsStore.put(previous)}}
const control={key:'active',activeFormat:FIELD_STORAGE_FORMAT,activeEpoch:epoch,activationId:`shared:${sessionId}:${updatedAt}`,activationVerified:true,switchedAt:updatedAt};
worldsStore.put({epoch,status:'active',schema:SAVE_SCHEMA,worldGeneration:WORLD_GENERATION,global:globalForStorage(data,updatedAt),boardCount:0,solvedCount:0,score:0,bounds:{minX:0,minY:0,maxX:1,maxY:1},approximateBytes:0,createdAt:updatedAt,activatedAt:updatedAt,source:{kind:'single-shared-world'}});
controlStore.put(control);await done;startupWorldControl=control;rememberWorldEpoch(epoch);return true;
}
function resetClientToSingleSharedWorld(){
const previous=data||defaultData(),fresh=defaultData();
for(const field of SHARED_PERSONAL_FIELDS)if(Object.prototype.hasOwnProperty.call(previous,field))fresh[field]=deepClone(previous[field]);
fresh.worldEpoch=createWorldEpoch();fresh.cloudRevision=0;fresh.worldFeedRevision=0;fresh.cloudPending=normalizeCloudPending(null);fresh.cameraAnchor=null;fresh.selectedBoardId=null;data=fresh;
for(const collection of[dirtyMetaIds,dirtyStateIds,deletedBoardIds,cloudJournalMetaIds,cloudJournalStateIds,cloudJournalDeletedIds,cloudOutboxDeleteKeys])collection.clear();
deletedBoardRevisions.clear();deletedBoardAuthors.clear();stateStatSignatures.clear();stateEconomySignatures.clear();boardIndexSummaries.clear();hydratedBoardLru.clear();adjacencyCache.clear();rendered.clear();boardClaims.clear();remotePlayers.clear();realtimeReactions.clear();
occupancy=new Map();closedVoidKeys=new Set();activeBoard=null;hudBoardId=null;hydratedBoardBytes=0;cloudJournalGlobalChanged=false;cloudJournalChangeSeq=0;globalDirty=false;globalChangeSeq=0;cloudPushPending=emptyCloudPending();lastCloudWorldGlobalSignature='';fieldIndexComplete=true;fieldIndexExpectedCount=0;fieldIndexLoadedCount=0;fieldIndexAfterNumber=-1;cloudOutboxReady=true;statsDirty=true;cachedStats={solved:0,score:0,earned:0};inventoryCache=null;spentScoreCache=null;
return data;
}
async function waitForRealtimeReady(timeout=7000){
if(realtimeReady)return true;connectRealtime();const started=Date.now();while(!realtimeReady&&Date.now()-started<timeout){await new Promise(resolve=>setTimeout(resolve,50));if(!cloudAvailable)break}return realtimeReady;
}
function sharedWorldGlobalForCloud(){return{schema:SAVE_SCHEMA,gameplayVersion:GAMEPLAY_DATA_VERSION,worldGeneration:WORLD_GENERATION,appVersion:APP_VERSION,generatorVersion:GENERATOR_VERSION,nextId:data.nextId,solved:data.solved||0,lastSolveAt:data.lastSolveAt||0,specialMechanicsSeen:normalizeSpecialMechanics(data.specialMechanicsSeen),quarantine:data.quarantine||{}}}
function sharedWorldGlobalSignature(){return JSON.stringify(sharedWorldGlobalForCloud())}
function mergeCloudPending(target,signal={}){
for(const id of signal.metaIds||[]){target.deleted.delete(id);target.metaIds.add(id)}
for(const id of signal.stateIds||[]){if(!data?.states?.[id])continue;target.deleted.delete(id);target.stateIds.add(id)}
for(const id of signal.deleted||[]){target.metaIds.delete(id);target.stateIds.delete(id);target.deleted.add(id)}
target.globalChanged=target.globalChanged||signal.globalChanged===true;return target;
}
function takeCloudPendingBatch(source,limit=16,preferredBoardId=null){
const batch=emptyCloudPending(),remainder=emptyCloudPending(),maximum=Math.max(1,Math.floor(limit)||1);
batch.globalChanged=source.globalChanged===true;
const boardIds=[...new Set([...(source.stateIds||[]),...(source.metaIds||[])])],preferred=boardIds.includes(preferredBoardId)?preferredBoardId:boardIds[0],selectedBoardIds=new Set(preferred?[preferred]:[]);
for(const key of['metaIds','stateIds'])for(const id of source[key]||[])(selectedBoardIds.has(id)?batch:remainder)[key].add(id);
let deletedUsed=selectedBoardIds.size;
for(const id of source.deleted||[])(deletedUsed++<maximum?batch:remainder).deleted.add(id);
return{batch,remainder};
}
function cloudPendingHasWork(pending=cloudPushPending){return pending.globalChanged||pending.metaIds.size>0||pending.stateIds.size>0||pending.deleted.size>0}
function cloudMutationId(payload,changeSeq){const signature=JSON.stringify(payload);let value=2166136261;for(let index=0;index<signature.length;index++)value=Math.imul(value^signature.charCodeAt(index),16777619);const hash=(value>>>0).toString(16).padStart(8,'0'),owner=String(sessionId||'client').replace(/[^A-Za-z0-9_-]/g,'').slice(0,40)||'client';return`${owner}-${Math.max(0,Number(changeSeq)||0).toString(36)}-${hash}`}
function cloudProfileIdentity(profile=data.cloudProfile){return profile?`${profile.playerId}.${profile.token}`:''}
function scheduleCloudCheckpointRetry(){if(cloudCheckpointRetryTimer)return;cloudCheckpointRetryTimer=setTimeout(()=>{cloudCheckpointRetryTimer=0;if(hasPendingPersistence())void persistNow({skipCloud:true}).then(ok=>{if(!ok)scheduleCloudCheckpointRetry()})},1000)}
function restoreCloudPushPending(){cloudPushPending=emptyCloudPending();mergeCloudPending(cloudPushPending,currentCloudPending())}
function acknowledgeCloudPending(pending,metaRevs,stateRevs,changeSeq){
for(const id of pending.metaIds)if(!dirtyMetaIds.has(id)&&data.metas[id]?.rev===metaRevs.get(id)){cloudJournalMetaIds.delete(id);cloudOutboxDeleteKeys.add(`meta:${id}`)}
for(const id of pending.stateIds)if(!dirtyStateIds.has(id)&&data.states[id]?.rev===stateRevs.get(id)){cloudJournalStateIds.delete(id);cloudOutboxDeleteKeys.add(`state:${id}`)}
for(const id of pending.deleted)if(!deletedBoardIds.has(id)&&!data.metas[id]){cloudJournalDeletedIds.delete(id);cloudOutboxDeleteKeys.add(`deleted:${id}`)}
if(cloudJournalChangeSeq===changeSeq){cloudJournalGlobalChanged=false;cloudOutboxDeleteKeys.add('global')}
data.cloudPending=currentCloudPending();restoreCloudPushPending();
}
function armCloudPush(delay=5000){clearTimeout(cloudPushTimer);cloudPushTimer=null;if(!cloudAvailable||!data.cloudProfile||cloudSyncing||!cloudPendingHasWork())return false;cloudPushTimer=setTimeout(()=>{cloudPushTimer=null;void pushCloudPending()},Math.max(0,delay));return true}
async function fetchJson(url,options={},timeout=5000){
const statusProbe=url==='/api/cloud/status'&&(!options.method||String(options.method).toUpperCase()==='GET'),bases=statusProbe?[cloudApiBaseUrl,...cloudApiBaseCandidates.filter(candidate=>candidate!==cloudApiBaseUrl)]:[cloudApiBaseUrl];let lastError=null;
for(let index=0;index<bases.length;index++){
const base=bases[index],controller=new AbortController(),timer=setTimeout(()=>controller.abort(),timeout);
try{
const endpoint=cloudEndpointUrl(url,base),response=await fetch(endpoint,{...options,signal:controller.signal,headers:{'content-type':'application/json',...(options.headers||{})}}),body=await response.json().catch(()=>({}));
if(statusProbe&&response.ok&&body?.available===true){cloudApiBaseUrl=base;cloudApiPhpBridgeActive=response.headers.get('x-linkfield-bridge')==='php'||base===cloudApiBridgeUrl;return body}
if(!response.ok){const error=new Error(body.error||`HTTP ${response.status}`);error.status=response.status;error.body=body;throw error}
if(statusProbe&&index<bases.length-1){lastError=new Error('Cloud status endpoint did not return a shared-world response');continue}
return body;
}catch(error){lastError=error;if(!statusProbe||index===bases.length-1)throw error}
finally{clearTimeout(timer)}
}
throw lastError||new Error('Cloud API is unavailable');
}
function cloudAuthHeaders(){if(!data.cloudProfile)return{};const value=`Bearer ${data.cloudProfile.playerId}.${data.cloudProfile.token}`;return{authorization:value,'x-linkfield-authorization':value}}
function applyPlayerEconomyEnvelope(raw){
const player=raw?.player||raw;if(!player||!Array.isArray(player.purchases))return false;
const purchases=new Map();for(const purchase of[...normalizePlayerPurchases(data.playerPurchases),...normalizePlayerPurchases(player.purchases)]){const key=purchase.purchaseId||`${purchase.boardId}:${purchase.itemId}`;const prior=purchases.get(key);if(!prior||(purchase.boughtAt||0)>=(prior.boughtAt||0))purchases.set(key,purchase)}
const previousStarter=data.starterLineColor,starter=validStarterLineColorId(player.starterLineColor)?player.starterLineColor:starterLineColorForPlayer(data.cloudProfile?.playerId);
data.playerPurchases=normalizePlayerPurchases(player.purchases);data.playerEarnedScore=Number.isSafeInteger(player.earnedScore)&&player.earnedScore>=0?player.earnedScore:0;data.starterLineColor=starter;if(!validLineColorItemId(data.lineColorStyle)||data.lineColorStyle===previousStarter)data.lineColorStyle=starter;
playerEconomyLoaded=true;invalidateEconomyCaches();syncCosmeticAppearance();markGlobalDirty(false);updateHud();if(openStoreBoardId)renderStorePanel();if(inventoryModal?.classList.contains('show'))renderInventoryPanel();return true;
}
async function pullPlayerEconomy(){if(!cloudAvailable||!data.cloudProfile)return false;try{const result=await fetchJson('/api/player/state',{headers:cloudAuthHeaders()});serverClockOffset=Number.isFinite(result.serverTime)?result.serverTime-Date.now():serverClockOffset;applyPlayerEconomyEnvelope(result);await persistNow({skipCloud:true});return true}catch(error){playerEconomyLoaded=false;console.warn('BEND FIELD: player economy pull failed',error);return false}}
async function buyPersonalStoreItem(boardId,itemId){const result=await fetchJson('/api/player/purchase',{method:'POST',headers:cloudAuthHeaders(),body:JSON.stringify({boardId,itemId})},10000);applyPlayerEconomyEnvelope(result);await persistNow({skipCloud:true});return result.purchase||null}
function setCloudStatus(){if(!cloudBtn)return;const state=!cloudAvailable?'offline':cloudSyncing?'syncing':realtimeReady?'online':'connecting';cloudBtn.dataset.state=state;cloudBtn.title=state==='online'?'共有サーバーに接続済み':state==='offline'?'共有サーバーに接続できません':'共有サーバーへ接続中';cloudBtn.setAttribute('aria-label',cloudBtn.title)}
function addClearFeedEvent(event){
if(!clearFeed||!event||typeof event.playerName!=='string')return;const item=document.createElement('div');item.className='clear-feed-item';const name=document.createElement('b'),details=document.createElement('span');name.textContent=`🎉 ${event.playerName}`;details.textContent=` (${Math.round(event.x||0)}, ${Math.round(event.y||0)}) · レベル${Math.max(1,Math.round(event.level||1))}`;item.append(name,details);clearFeed.prepend(item);while(clearFeed.children.length>3)clearFeed.lastElementChild?.remove();setTimeout(()=>{item.classList.add('is-leaving');setTimeout(()=>item.remove(),380)},10000)
}
function applyCloudEnvelope(result,{initial=false}={}){
let changed=false;if(result?.player?.name&&data.playerName!==result.player.name){data.playerName=String(result.player.name).slice(0,24);changed=true}if(Array.isArray(result?.player?.purchases))applyPlayerEconomyEnvelope(result);
if(result?.clearEventsGap===true&&!initial){console.warn('LinkField: shared clear-event history gap; state was resynchronized from the authoritative snapshot');toast('\u5171\u6709\u5c65\u6b74\u306e\u4e00\u90e8\u3092\u518d\u540c\u671f\u3057\u307e\u3057\u305f\u3002',3000)}
const latest=Math.max(0,Number(result?.latestEventRevision)||0),events=Array.isArray(result?.clearEvents)?result.clearEvents:[];
for(const event of events)if(event?.id)removeClaim(event.id,'cleared');if(initial&&!(data.worldFeedRevision>0)){data.worldFeedRevision=latest;changed=true}else{for(const event of events)if((event.revision||0)>(data.worldFeedRevision||0))addClearFeedEvent(event);if(latest>(data.worldFeedRevision||0)){data.worldFeedRevision=latest;changed=true}}
if(changed)markGlobalDirty(false);return changed;
}
async function createCloudProfile(){const result=await fetchJson('/api/cloud/session',{method:'POST',body:JSON.stringify({name:data.playerName||''})});serverClockOffset=result.serverTime-Date.now();const previousStarter=data.starterLineColor;data.cloudProfile={playerId:result.playerId,token:result.token};data.playerName=result.name||data.playerName||null;if(typeof validStarterLineColorId==='function'&&validStarterLineColorId(result.starterLineColor)){data.starterLineColor=result.starterLineColor;if(!validLineColorItemId(data.lineColorStyle)||data.lineColorStyle===previousStarter)data.lineColorStyle=result.starterLineColor;if(typeof syncCosmeticAppearance==='function')syncCosmeticAppearance()}data.cloudRevision=0;markGlobalDirty(false);if(!await persistNow({skipCloud:true})){scheduleCloudCheckpointRetry();throw new Error('Shared-world profile could not be committed locally')}return data.cloudProfile}
async function pullCloudWorld(force=false,sinceOverride=null,{initial=false,authoritative=false}={}){
if(typeof fieldArchiveBusy!=='undefined'&&fieldArchiveBusy)return false;if(!cloudAvailable||!data.cloudProfile||cloudSyncing&&!force)return false;
if(interactionActive()||[...rendered.values()].some(board=>board.drawing?.keyboardActive))return false;if(hasPendingPersistence()&&!await flushSave())return false;
const profileIdentity=cloudProfileIdentity();cloudSyncing=true;setCloudStatus('共有取得中','syncing');let pulled=false,reopenedAfterPull=0;
try{
const previousRevision=sinceOverride??data.cloudRevision??0,remoteIds=new Set();let cursor=0,targetRevision=null,changed=false,fullSnapshot=false,firstPage=true,envelopeChanged=false,authoritativeWorld=authoritative===true;
do{
const query=`/api/cloud/pull?since=${previousRevision}&eventsSince=${data.worldFeedRevision||0}${cursor?`&cursor=${cursor}&at=${targetRevision}`:''}`,result=await fetchJson(query,{headers:cloudAuthHeaders()},30000);
await waitForInteractionSettle();
if(cloudProfileIdentity()!==profileIdentity)throw new Error('Shared-world profile changed while pulling');serverClockOffset=result.serverTime-Date.now();targetRevision??=result.revision||0;if(initial&&previousRevision===0&&targetRevision>0)authoritativeWorld=true;if(result.revision!==targetRevision)throw new Error('Shared world changed during paged pull');
if(firstPage){envelopeChanged=applyCloudEnvelope(result,{initial})||envelopeChanged;firstPage=false}changed=result.changed===true;fullSnapshot=result.fullSnapshot===true;
if(changed&&result.page){
const external={...(result.page.global||{}),worldEpoch:data.worldEpoch,metas:{},states:{}};
for(const[id,raw]of Object.entries(result.page.metas||{})){const meta=normalizeMeta(id,raw);if(!meta)throw new Error(`Shared metadata is invalid: ${id}`);external.metas[id]=meta;remoteIds.add(id)}
for(const[id,raw]of Object.entries(result.page.states||{}))external.states[id]=normalizeState(raw);
cloudApplyingRemote=true;try{mergeSnapshotIntoData(external,{finalize:false,authoritativeWorld});for(const id of result.page.deleted||[])if(data.metas[id]&&!cloudJournalMetaIds.has(id)&&!cloudJournalStateIds.has(id)){delete data.metas[id];delete data.states[id];markBoardDeleted(id);destroyBoard(rendered.get(id))}for(const id of Object.keys(external.metas))dirtyMetaIds.add(id);for(const id of Object.keys(external.states))dirtyStateIds.add(id)}finally{cloudApplyingRemote=false}
}
cursor=Number.isSafeInteger(result.nextCursor)?result.nextCursor:0;
}while(cursor);
data.cloudRevision=targetRevision||0;lastCloudWorldGlobalSignature=sharedWorldGlobalSignature();
if(changed){cloudApplyingRemote=true;try{if(fullSnapshot)for(const id of Object.keys(data.metas))if(!remoteIds.has(id)&&(authoritativeWorld||!cloudJournalMetaIds.has(id)&&!cloudJournalStateIds.has(id))){clearSharedWorldJournalRow('meta',id);clearSharedWorldJournalRow('state',id);delete data.metas[id];delete data.states[id];markBoardDeleted(id);destroyBoard(rendered.get(id))}resolveMergedOverlaps();statsDirty=true;markGlobalDirty(false)}finally{cloudApplyingRemote=false}reopenedAfterPull=reopenMissingGateExpansions();refreshWorldView({rebuild:true,syncConnections:true,persist:false});if(!await persistNow({skipCloud:true})){scheduleCloudCheckpointRetry();throw new Error('Shared-world pull could not be committed locally')}}else if(data.cloudRevision!==previousRevision||envelopeChanged){markGlobalDirty(false);if(!await persistNow({skipCloud:true})){scheduleCloudCheckpointRetry();throw new Error('Shared-world revision could not be committed locally')}}
setCloudStatus();pulled=true;return true;
}catch(error){setCloudStatus();console.warn('BEND FIELD: shared-world pull failed',error);return false}
finally{cloudSyncing=false;setCloudStatus();restoreCloudPushPending();if(reopenedAfterPull)scheduleExpansionRepair(150);if(cloudPendingHasWork())armCloudPush(pulled?250:5000)}
}
function scheduleCloudPush(signal={},delay=120){mergeCloudPending(cloudPushPending,signal);armCloudPush(delay)}
async function pushCloudPending(preferredBoardId=null){
clearTimeout(cloudPushTimer);cloudPushTimer=null;if(typeof fieldArchiveBusy!=='undefined'&&fieldArchiveBusy){armCloudPush(1000);return false}if(!cloudAvailable||!data.cloudProfile||cloudSyncing||!cloudPendingHasWork())return false;
const{batch:pending,remainder}=takeCloudPendingBatch(cloudPushPending,16,preferredBoardId),profileIdentity=cloudProfileIdentity();cloudPushPending=remainder;
const globalSignature=sharedWorldGlobalSignature();if(!pending.metaIds.size&&!pending.stateIds.size&&!pending.deleted.size&&pending.globalChanged&&globalSignature===lastCloudWorldGlobalSignature){cloudJournalGlobalChanged=false;cloudOutboxDeleteKeys.add('global');data.cloudPending=currentCloudPending();restoreCloudPushPending();if(!await persistNow({skipCloud:true})){scheduleCloudCheckpointRetry();return false}return true}
cloudSyncing=true;setCloudStatus('共有送信中','syncing');let payload=null,sentMetaRevs=new Map(),sentStateRevs=new Map(),sentCloudChangeSeq=cloudJournalChangeSeq,retryDelay=250;
try{
const rows=await cloudRowsForStorage(pending.metaIds,pending.stateIds);payload={baseRevision:data.cloudRevision||0,global:sharedWorldGlobalForCloud(),metas:rows.metas,states:rows.states,deleted:[]};payload.mutationId=cloudMutationId(payload,sentCloudChangeSeq);sentMetaRevs=new Map(payload.metas.map(meta=>[meta.id,meta.rev||0]));sentStateRevs=new Map(payload.states.map(row=>[row.id,row.value?.rev||0]));
const result=await fetchJson('/api/cloud/push',{method:'POST',headers:cloudAuthHeaders(),body:JSON.stringify(payload)},30000);if(cloudProfileIdentity()!==profileIdentity){mergeCloudPending(cloudPushPending,pending);retryDelay=5000;setCloudStatus();return false}
serverClockOffset=result.serverTime-Date.now();data.cloudRevision=result.revision;applyCloudEnvelope(result);lastCloudWorldGlobalSignature=sharedWorldGlobalSignature();acknowledgeCloudPending(pending,sentMetaRevs,sentStateRevs,sentCloudChangeSeq);if(!await persistNow({skipCloud:true})){scheduleCloudCheckpointRetry();setCloudStatus();return false}setCloudStatus();return true;
}catch(error){mergeCloudPending(cloudPushPending,pending);if(error.status===423&&payload){cloudSyncing=false;const boardId=error.body?.boardId||payload.states?.[0]?.id,claimed=boardId?await requestBoardClaim(boardId,{allowLocalSolved:true}):false;if(!claimed){const pulled=await pullCloudWorld(true,0,{authoritative:true});retryDelay=pulled?500:5000}else retryDelay=0;setCloudStatus()}else if(error.status===409&&payload){cloudSyncing=false;const pulled=await pullCloudWorld(true,payload.baseRevision,{authoritative:payload.baseRevision===0});retryDelay=pulled?250:5000}else if(error.status===400&&payload?.metas?.length){cloudSyncing=false;const pulled=await pullCloudWorld(true,0,{authoritative:true});retryDelay=pulled?500:5000;setCloudStatus()}else{retryDelay=5000;setCloudStatus();console.warn('BEND FIELD: shared-world push failed',error)}return false}
finally{cloudSyncing=false;setCloudStatus();if(cloudPendingHasWork())armCloudPush(retryDelay)}
}
function scheduleWorldPoll(delay=document.visibilityState==='visible'?30000:120000){clearTimeout(worldPollTimer);if(!cloudAvailable||!data.cloudProfile)return;worldPollTimer=setTimeout(async()=>{worldPollTimer=0;if(document.visibilityState==='visible')await pullCloudWorld();scheduleWorldPoll()},Math.max(5000,delay))}
function scheduleRemoteWorldPull(revision,delay=60){
remoteWorldRevision=Math.max(remoteWorldRevision,Math.max(0,Number(revision)||0));if(remoteWorldRevision<=(data.cloudRevision||0))return false;
clearTimeout(remoteWorldPullTimer);remoteWorldPullTimer=setTimeout(async()=>{remoteWorldPullTimer=0;if(remoteWorldRevision<=(data.cloudRevision||0))return;const pulled=await pullCloudWorld();if(!pulled&&remoteWorldRevision>(data.cloudRevision||0))scheduleRemoteWorldPull(remoteWorldRevision,500)},Math.max(0,delay));return true;
}
async function fetchCurrentSharedWorldStatus(){
let lastStatus=null,lastError=null;
for(let attempt=0;attempt<6;attempt++){
try{
const status=await fetchJson('/api/cloud/status',{},8000);lastStatus=status;
const identityOk=status.available===true&&status.sharedWorld===true&&status.singleWorld===true&&status.worldId==='link-field-main';
const generationOk=status.worldGeneration===WORLD_GENERATION;
const versionOk=status.appVersion===APP_VERSION;
if(identityOk&&generationOk&&versionOk)return status;
}catch(error){lastError=error}
if(attempt<5)await new Promise(resolve=>setTimeout(resolve,400));
}
if(lastStatus)throw new Error(`共有サーバーの版が一致しません。サーバー側で npm start を実行してください。(ブラウザ v${APP_VERSION}/サーバー v${lastStatus.appVersion||'不明'}`);
throw lastError||new Error('単一共有ワールドへ接続できません。サーバーを確認してください。');
}
async function initCloudSync({startup=false}={}){
let status;try{status=await fetchCurrentSharedWorldStatus()}catch(error){cloudAvailable=false;setCloudStatus();throw error}
cloudAvailable=true;serverClockOffset=status.serverTime-Date.now();cloudOutboxReady=true;
if(!data.cloudProfile)await createCloudProfile();
if(startup){
const serverRevision=Math.max(0,Number(status.revision)||0);resetClientToSingleSharedWorld();await activateSingleSharedClientCache();
if(serverRevision>0){if(!await pullCloudWorld(true,0,{initial:true,authoritative:true}))throw new Error('共有盤面を取得できませんでした。')}
else{
await ensureStart();restoreCloudPending({metaIds:Object.keys(data.metas),stateIds:Object.keys(data.states),deleted:[],globalChanged:true});restoreCloudPushPending();
if(!await persistNow({skipCloud:true}))throw new Error('共有盤面の初期キャッシュを準備できませんでした。');
if(!await pushCloudPending()){
const refreshed=await fetchJson('/api/cloud/status',{},8000);if((Number(refreshed.revision)||0)<=0)throw new Error('共有盤面を初期化できませんでした。');
if(!await pullCloudWorld(true,0,{initial:true,authoritative:true}))throw new Error('共有盤面を取得できませんでした。');
}else if(!await pullCloudWorld(true,0,{initial:true,authoritative:true}))throw new Error('共有盤面を確認できませんでした。');
}
}else if(!await pullCloudWorld(false,null,{initial:false}))return false;
await pullPlayerEconomy();setCloudStatus();connectRealtime();if(!await waitForRealtimeReady())throw new Error('盤面占有サービスへ接続できませんでした。');scheduleRealtimeViewport(true);scheduleWorldPoll();return true;
}
async function changePlayerNameFromTopUi(){
const entered=prompt('プレイヤー名を入力してください24文字まで。',currentPlayerName());if(entered==null)return;
try{const name=await commitPlayerProfileName(entered);toast(`プレイヤー名を「${name}」に変更しました。`)}
catch(error){toast('名前を変更できませんでした。')}
}
if(cloudBtn)cloudBtn.onclick=null;
async function init(){
deleteRetiredWorldData();
data=attachWorldEpoch(await readInitialDataAsync(),startupWorldEpoch);const generatedInitialPlayerName=!data.playerName||!String(data.playerName).trim();if(generatedInitialPlayerName)data.playerName=createAutomaticPlayerName();seedRevisionClock(data);if(generatedInitialPlayerName)markGlobalDirty(false);
for(const[id,tombstone]of recoveredDeletionTombstones)if(!data.metas[id]){deletedBoardIds.add(id);deletedBoardRevisions.set(id,tombstone.rev||0);deletedBoardAuthors.set(id,tombstone.revAuthor||'')}
for(const id of startupRecoveredMetaIds)if(data.metas[id])dirtyMetaIds.add(id);
for(const id of startupRecoveredStateIds)if(data.states[id])dirtyStateIds.add(id);
for(const id of startupRecoveredDeletedIds)if(!data.metas[id]){deletedBoardIds.add(id);deletedBoardRevisions.set(id,nextRevision());deletedBoardAuthors.set(id,sessionId)}
if(startupRecoveredMetaIds.size||startupRecoveredStateIds.size||startupRecoveredDeletedIds.size)markGlobalDirty(false);
cloudOutboxReady=true;restoreCloudPending(normalizeCloudPending(null));restoreCloudPushPending();
normalizeEquippedCosmeticsInPlace(data);data.debugAllItems=false;applyUiSettings();syncCursorAppearance(data.cursorStyle||'default');syncCosmeticAppearance();document.body.dataset.debugItems=debugAllItemsEnabled()?'on':'off';
stateStatSignatures.clear();stateEconomySignatures.clear();for(const[id,state]of Object.entries(data.states)){normalizedStateObjects.add(state);if(!state._summaryOnly){stateStatSignatures.set(id,stateStatSignature(state));stateEconomySignatures.set(id,stateEconomySignature(state))}}
orphanPruneDirty=true;statsDirty=true;invalidateEconomyCaches();
await initCloudSync({startup:true});
await ensureStart();
let initialMeta=await randomUnsolvedMeta()||data.metas.B0||Object.values(data.metas)[0];
try{
if(initialMeta&&!initialMeta.puzzle)await hydrateMeta(initialMeta);
if(!data.metas.B0?.puzzle&&data.metas.B0)await hydrateMeta(data.metas.B0);
if(!data.metas.B0?.puzzle)throw new Error('\u539f\u70b9\u306e\u76e4\u9762\u3092\u8aad\u307f\u8fbc\u3081\u307e\u305b\u3093\u3002');
if(startupWorldControl?.activationVerified===false)await validateActiveWorldReadiness(data.worldEpoch);
}catch(error){
if(startupWorldControl?.activationVerified===false&&idbAvailable){const restored=await rollbackUnverifiedWorld(await openWorldDb(),startupWorldControl).catch(()=>null);if(restored){lifecyclePersistenceSuppressed=true;rememberWorldEpoch(restored.activeEpoch);announceWorldReplacement(restored.activeEpoch);location.reload();return}}
throw error;
}
refreshWorldView({rebuild:true,syncConnections:false,resumeTimer:true});
requestAnimationFrame(()=>{centerMeta(initialMeta||data.metas.B0,{select:false});scheduleMinimap(true);scheduleNoiseBackground(true)});
document.body.classList.remove('loading');document.body.dataset.ready='true';document.body.dataset.generator='gate-procedural-v47';
if(!fieldIndexComplete)setTimeout(()=>{void completeV2IndexScan().catch(error=>{console.warn('BEND FIELD: field index scan failed',error);showStatus(`\u30d5\u30a3\u30fc\u30eb\u30c9\u7d22\u5f15\u3092\u8aad\u307f\u8fbc\u3081\u307e\u305b\u3093\u3067\u3057\u305f\u3002 ${error?.message||error}`,{retry:true,fresh:false})})},0);
openHelp(true);
if(loadNotices.length)showStatus(loadNotices.join(' '),{retry:false,fresh:true});
if(hasPendingPersistence())void save(true);
// Warm the generator process only; no surrounding puzzle is generated before a solved gate opens it.
setTimeout(()=>{createPuzzleWorker();if(navigator.storage?.persist)void navigator.storage.persist().catch(()=>{})},0);
setTimeout(()=>{void collectV2Garbage().catch(error=>console.warn('BEND FIELD: epoch cleanup deferred',error))},1500);
setTimeout(()=>{
reopenMissingGateExpansions();
if(pendingExpansionCount())void repairExpansions().catch(handleExpansionError);
},100);
}
init().catch(error=>{
console.error(error);document.body.classList.remove('loading');document.body.dataset.ready='error';document.body.dataset.error=error?.message||String(error);
preserveRecovery(readCompactMirrorRaw()||safeLocalGet(storageKey),'Startup failed');
showStatus(`\u30b2\u30fc\u30e0\u3092\u8d77\u52d5\u3067\u304d\u307e\u305b\u3093\uff1a${error?.message||error}`,{retry:true,fresh:false,onRetry:()=>location.reload()});toast('\u30b2\u30fc\u30e0\u3092\u8d77\u52d5\u3067\u304d\u307e\u305b\u3093\u3002',4000);
});
document.addEventListener('click',event=>{const button=event.target.closest?.('button');if(button&&!button.disabled&&!button.closest('.board-actions')&&!button.classList.contains('store-buy')&&!button.classList.contains('line-store'))playSound('click')},{capture:true});