'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,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=180; 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[cloudApiBridgeUrl,`${cloudAppBaseUrl}api/`,(()=>{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/'; 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 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;indexCLOCK_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=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{ 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.pathIndex0?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;indexckey(...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;indexLOCAL_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{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.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&×tamp-fpsLastFrameAt180; 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>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;indexsoundTone(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(){ 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>>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&¤t?.solved===true&&incoming?.solved!==true, retainClaimedPending=compatible&¤t?.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)&¤t.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{ 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=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()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=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&¤tRecoverySeq>=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=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)&&attemptspair?.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;girefreshWorldView(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(){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;gateIndexpair?.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;gateIndexpair?.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;gateIndexpair?.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;gateIndexpair?.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=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()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;ib.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;keyIndexb.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!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!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]?'S':b[1]{ for(let cut=1;cut{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;indexschedule.maxCells){if(indextargetRange.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.lengthactualRange.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=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.lengthactualRange.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&&!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;indexrange.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(cycle0)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{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[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=0&&path.endGate0&&!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{ if(!minimapFrame)return; if(interactionActive('overview')){minimapFrame=0;return} if(timestamp-minimapLastDraw{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(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;indexsplitLinePathKey(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{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;indexsegment.length>0); } function pathStrokePieces(segments,startColor,endColor){ const pieces=[];let total=0; for(const points of segments)for(let i=1;i[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='ショップ'; 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(largestady; 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;pieceIndexpath.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{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;index0,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{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{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();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=indexsameCell(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=Math.abs(dr)); if(chooseHorizontal){c+=sx;maxC+=deltaC}else{r+=sy;maxR+=deltaR} }else if(maxC=0&&c>=0&&rsameCell(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{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(dvaluemax-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=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()&&processed48)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{}){ 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&×tamp+INTERACTION_FRAME_TOLERANCE_MSselectBoard(b)); b.svg.addEventListener('pointerdown',async e=>{ const endpointTarget=e.target.closest?.('.endpoint-hit'),gateTarget=e.target.closest?.('.gate-hit'); if(e.button!==0||b.joiningPaths||!endpointTarget&&!gateTarget||!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)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&×tamp-overviewInteractionLastBuild{ 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{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(changesdata.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(changesdata.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&×tamp-cameraLastDraw{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&×tamp+INTERACTION_FRAME_TOLERANCE_MS{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)}`, 'https://host.nishi.boats/~333/link-field/' ].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.scorepurchaseStoreItem(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.scoreentry.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=0&&path.endGate0&&!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;position0&&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(;storeIndexString(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){}); 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{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{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,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(),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(realtimePollSequence,Number(envelope.sequence));for(const message of Array.isArray(envelope.messages)?envelope.messages:[])handleRealtimeMessage(message);return true} function scheduleRealtimeHttpPoll(delay=450){clearTimeout(realtimePollTimer);realtimePollTimer=0;if(realtimeTransport!=='http-poll'||!realtimePresenceId||document.visibilityState==='hidden')return false;realtimePollTimer=setTimeout(()=>{realtimePollTimer=0;void enqueueRealtimeHttp(async()=>{try{const query=new URLSearchParams({presenceId:realtimePresenceId,after:String(realtimePollSequence)}),result=await fetchJson(`/api/realtime/poll?${query}`,{headers:cloudAuthHeaders()},5000);applyRealtimeHttpEnvelope(result);scheduleRealtimeHttpPoll(350)}catch(error){if(error?.status===410){realtimeReady=false;realtimePresenceId=null;realtimeTransport='none'}scheduleRealtimeReconnect(1200)}})},Math.max(100,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 wait=REALTIME_CURSOR_INTERVAL-(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-presenceLastDrawREALTIME_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{ const particles=[];for(let index=0;index.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&×tamp+1{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=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;ringIndex0){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(){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 removeClaim(boardId,reason='released'){const claim=boardClaims.get(boardId);boardClaims.delete(boardId);if(realtimeOwnClaimBoardId===boardId)realtimeOwnClaimBoardId=null;refreshClaimPresentation(boardId);const board=rendered.get(boardId);if(claim?.playerId===currentPlayerId()&&board?.drawing?.pointerId!=null&&reason!=='cleared')cancelPointerGestures()} 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 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;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();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')return removeClaim(message.boardId,message.reason); 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();} } 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')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;resetRealtimeConnectionState();scheduleRealtimeReconnect()};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){ if(metaState(boardId)?.solved||!cloudAvailable||!data.cloudProfile)return false; const existing=currentBoardClaim(boardId),now=trustedNow(); if(existing?.playerId!==currentPlayerId()&&existing?.expiresAt>now)return false; if(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-realtimeLastClaimTouchAtrealtimeHeldPointers.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&¤t.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()-startedsetTimeout(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=512){ const batch=emptyCloudPending(),remainder=emptyCloudPending(),maximum=Math.max(1,Math.floor(limit)||1);let used=0; batch.globalChanged=source.globalChanged===true; for(const key of['metaIds','stateIds','deleted'])for(const id of source[key]||[])(used0||pending.stateIds.size>0||pending.deleted.size>0} 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;indexcontroller.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;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=(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); 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; 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}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(cloudPendingHasWork())armCloudPush(pulled?250:5000)} } function scheduleCloudPush(signal={},delay=350){mergeCloudPending(cloudPushPending,signal);armCloudPush(delay)} async function pushCloudPending(){ 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),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:[]};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 pulled=await pullCloudWorld(true,0,{authoritative:true});retryDelay=pulled?500:5000;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'?1200:10000){clearTimeout(worldPollTimer);if(!cloudAvailable||!data.cloudProfile)return;worldPollTimer=setTimeout(async()=>{worldPollTimer=0;if(document.visibilityState==='visible')await pullCloudWorld();scheduleWorldPoll()},Math.max(1000,delay))} 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});