var GAME_VERSION = "0.23"; var MAP_COUNT = 7; var DIFFICULTY_COUNT = 3; var VALID_MODES = ["mission", "endless", "coop_ai", "coop_online"]; var MATCH_MODULE = "void_bastion_coop"; var ROOM_COLLECTION = "private_rooms"; var ACTIVITY_COLLECTION = "player_activity"; var ACTIVITY_KEY = "summary"; var STATS_COLLECTION = "internal_stats"; var STATS_KEY = "global"; var SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000"; var OP_SNAPSHOT = 1, OP_EVENT = 2, OP_BUILD = 10, OP_UPGRADE = 11, OP_SELL = 12, OP_START_WAVE = 13, OP_READY = 14; var TICK_RATE = 10, SNAPSHOT_TICKS = 2, GRID = 80, WORLD_WIDTH = 2960, MIRROR_X = 1480; var REACTOR_X = 1480, REACTOR_Y = 440, REACTOR_RADIUS = 112, PLAYFIELD_TOP = 92, PLAYFIELD_BOTTOM = 758; var LEFT_PATH = [[-40,120],[1160,120],[1160,360],[360,360],[360,680],[1400,680],[1400,520],[1480,520]]; var RIGHT_PATH = []; for (var pathIndex = 0; pathIndex < LEFT_PATH.length; pathIndex++) RIGHT_PATH.push([WORLD_WIDTH - LEFT_PATH[pathIndex][0], LEFT_PATH[pathIndex][1]]); var PATHS = [LEFT_PATH, RIGHT_PATH]; var TOWERS = [ {cost:120,damage:22,range:190,cooldown:0.55,targetClass:"ground"}, {cost:180,damage:12,range:165,cooldown:0.18,targetClass:"both",bothScale:0.82}, {cost:250,damage:65,range:260,cooldown:1.40,targetClass:"ground"}, {cost:230,damage:34,range:240,cooldown:1.05,targetClass:"both",bothScale:0.72}, {cost:210,damage:20,range:215,cooldown:0.92,targetClass:"both",bothScale:1}, {cost:300,damage:77,range:330,cooldown:1.85,targetClass:"both",bothScale:0.70} ]; var ENEMIES = [ {category:"air",hp:70,speed:105,reward:18,breachDamage:1}, {category:"air",hp:185,speed:72,reward:34,breachDamage:2}, {category:"air",hp:520,speed:43,reward:78,breachDamage:3}, {category:"air",hp:52,speed:178,reward:22,breachDamage:2}, {category:"air",hp:260,speed:58,reward:52,breachDamage:2}, {category:"air",hp:740,speed:48,reward:108,breachDamage:4}, {category:"ground",hp:88,speed:142,reward:22,breachDamage:1}, {category:"ground",hp:390,speed:61,reward:61,breachDamage:3}, {category:"ground",hp:980,speed:36,reward:132,breachDamage:5} ]; var DIFFICULTIES = [ {hp:1,speed:1,count:1,income:1,spawnDelay:1,startCredits:450}, {hp:1.32,speed:1.07,count:1.24,income:0.94,spawnDelay:0.91,startCredits:430}, {hp:1.72,speed:1.15,count:1.48,income:0.84,spawnDelay:0.82,startCredits:400} ]; function clampInteger(value, minimum, maximum) { var parsed = Math.floor(Number(value)); if (!isFinite(parsed) || parsed < minimum || parsed > maximum) throw new Error("numeric value outside allowed range"); return parsed; } function leaderboardId(mapIndex, difficulty, mode) { return mode + "_map" + mapIndex + "_difficulty" + difficulty; } function rpcServerInfo(ctx, logger, nk, payload) { return JSON.stringify({game:"void_bastion",version:GAME_VERSION,leaderboards:MAP_COUNT*DIFFICULTY_COUNT*VALID_MODES.length,authoritative_multiplayer:true,status:"online"}); } function normalizeMetricLabel(value, fallback) { var normalized = String(value || "").toLowerCase().replace(/[^a-z0-9._-]/g, "").substring(0, 32); return normalized || fallback; } function utcDay(timestamp) { return new Date(timestamp).toISOString().slice(0, 10); } function readStorageRecord(nk, collection, key, userId) { var records = nk.storageRead([{collection:collection,key:key,userId:userId}]); return records.length ? records[0] : null; } function emptyStats(now) { return {total_players:0,platforms:{},versions:{},daily_active:{},created_at:now,updated_at:now}; } function incrementMetric(metrics, key, amount) { metrics[key] = Math.max(0, Math.floor(Number(metrics[key] || 0)) + amount); if (metrics[key] === 0) delete metrics[key]; } function trimDailyActivity(daily, currentDay) { var minimum = Date.parse(currentDay + "T00:00:00.000Z") - 35 * 86400000; var days = Object.keys(daily); for (var i = 0; i < days.length; i++) { if (Date.parse(days[i] + "T00:00:00.000Z") < minimum) delete daily[days[i]]; } } function writeStats(nk, previousRecord, value) { var write = {collection:STATS_COLLECTION,key:STATS_KEY,userId:SYSTEM_USER_ID,value:value,permissionRead:0,permissionWrite:0}; if (previousRecord && previousRecord.version) write.version = previousRecord.version; else write.version = "*"; nk.storageWrite([write]); } function updatePlayerStats(nk, playerRecord, platform, version, day, now) { for (var attempt = 0; attempt < 3; attempt++) { var statsRecord = readStorageRecord(nk, STATS_COLLECTION, STATS_KEY, SYSTEM_USER_ID); var stats = statsRecord && statsRecord.value ? statsRecord.value : emptyStats(now); stats.platforms = stats.platforms || {}; stats.versions = stats.versions || {}; stats.daily_active = stats.daily_active || {}; if (!playerRecord) { stats.total_players = Math.max(0, Math.floor(Number(stats.total_players || 0))) + 1; incrementMetric(stats.platforms, platform, 1); incrementMetric(stats.versions, version, 1); } else { var oldPlatform = normalizeMetricLabel(playerRecord.value.platform, "unknown"); var oldVersion = normalizeMetricLabel(playerRecord.value.version, "unknown"); if (oldPlatform !== platform) { incrementMetric(stats.platforms, oldPlatform, -1); incrementMetric(stats.platforms, platform, 1); } if (oldVersion !== version) { incrementMetric(stats.versions, oldVersion, -1); incrementMetric(stats.versions, version, 1); } } if (!playerRecord || String(playerRecord.value.last_seen_day || "") !== day) incrementMetric(stats.daily_active, day, 1); trimDailyActivity(stats.daily_active, day); stats.updated_at = now; try { writeStats(nk, statsRecord, stats); return; } catch (error) { if (attempt === 2) throw error; } } } function removePlayerStats(nk, playerRecord, now) { if (!playerRecord || !playerRecord.value) return; for (var attempt = 0; attempt < 3; attempt++) { var statsRecord = readStorageRecord(nk, STATS_COLLECTION, STATS_KEY, SYSTEM_USER_ID); if (!statsRecord || !statsRecord.value) return; var stats = statsRecord.value; stats.platforms = stats.platforms || {}; stats.versions = stats.versions || {}; stats.total_players = Math.max(0, Math.floor(Number(stats.total_players || 0)) - 1); incrementMetric(stats.platforms, normalizeMetricLabel(playerRecord.value.platform, "unknown"), -1); incrementMetric(stats.versions, normalizeMetricLabel(playerRecord.value.version, "unknown"), -1); stats.updated_at = now; try { writeStats(nk, statsRecord, stats); return; } catch (error) { if (attempt === 2) throw error; } } } function rpcRecordActivity(ctx, logger, nk, payload) { if (!ctx.userId) throw new Error("authentication required"); var request; try { request = JSON.parse(payload || "{}"); } catch (error) { request = {}; } var platform = normalizeMetricLabel(request.platform, "unknown"); var version = normalizeMetricLabel(request.version, "unknown"); var now = Date.now(), day = utcDay(now); var playerRecord = readStorageRecord(nk, ACTIVITY_COLLECTION, ACTIVITY_KEY, ctx.userId); updatePlayerStats(nk, playerRecord, platform, version, day, now); nk.storageWrite([{collection:ACTIVITY_COLLECTION,key:ACTIVITY_KEY,userId:ctx.userId,value:{first_seen:playerRecord&&playerRecord.value?Number(playerRecord.value.first_seen||now):now,last_seen:now,last_seen_day:day,platform:platform,version:version},permissionRead:0,permissionWrite:0}]); return JSON.stringify({accepted:true}); } function rpcPrivateStats(ctx, logger, nk, payload) { if (ctx.userId) throw new Error("server-to-server access required"); var record = readStorageRecord(nk, STATS_COLLECTION, STATS_KEY, SYSTEM_USER_ID); var stats = record && record.value ? record.value : emptyStats(Date.now()); return JSON.stringify(stats); } function rpcSubmitRun(ctx, logger, nk, payload) { if (!ctx.userId) throw new Error("authentication required"); var run; try { run = JSON.parse(payload || "{}"); } catch (error) { throw new Error("invalid JSON payload"); } var mapIndex = clampInteger(run.map,0,MAP_COUNT-1), difficulty = clampInteger(run.difficulty,0,DIFFICULTY_COUNT-1); var mode = String(run.mode || ""); if (VALID_MODES.indexOf(mode) < 0) throw new Error("invalid game mode"); var wave = clampInteger(run.wave,1,mode === "mission" ? 15 : 9999), score = clampInteger(run.score,0,1000000000); if (score > wave*5000+100000) throw new Error("implausible score"); var id = leaderboardId(mapIndex,difficulty,mode); var publicName = "PILOT"; try { var account = nk.accountGetId(ctx.userId); var storedName = account && account.user ? normalizeDisplayName(account.user.displayName) : ""; if (!validateDisplayName(storedName)) publicName = storedName; } catch (error) { logger.warn("Could not resolve display name for leaderboard user %s: %s", ctx.userId, error); } var metadata = {map:mapIndex,difficulty:difficulty,mode:mode,wave:wave,displayName:publicName,clientVersion:String(run.client_version||"unknown"),submittedAt:Date.now()}; var record = nk.leaderboardRecordWrite(id,ctx.userId,publicName,score,wave,metadata,null); return JSON.stringify({accepted:true,leaderboard_id:id,rank:record.rank,score:Number(record.score),wave:wave}); } function normalizeDisplayName(value) { return String(value || "").trim().replace(/\s+/g, " ").substring(0, 20); } function validateDisplayName(value) { var name = normalizeDisplayName(value); if (name.length < 3) return "name_too_short"; if (name.length > 20) return "name_too_long"; if (!/^[A-Za-z0-9 ÄÖÜäöüß_-]+$/.test(name)) return "invalid_characters"; var reserved = ["ADMIN","ADMINISTRATOR","GERGAMES","MODERATOR","SUPPORT","SYSTEM"]; if (reserved.indexOf(name.toUpperCase()) >= 0) return "reserved_name"; return ""; } function rpcUpdateDisplayName(ctx, logger, nk, payload) { if (!ctx.userId) throw new Error("authentication required"); var request; try { request = JSON.parse(payload || "{}"); } catch (error) { return JSON.stringify({accepted:false,error:"invalid_json"}); } var name = normalizeDisplayName(request.display_name), errorCode = validateDisplayName(name); if (errorCode) return JSON.stringify({accepted:false,error:errorCode}); nk.accountUpdateId(ctx.userId, null, name, null, null, null, null, null); return JSON.stringify({accepted:true,display_name:name}); } function rpcDeleteAccount(ctx, logger, nk, payload) { if (!ctx.userId) throw new Error("authentication required"); var playerRecord = readStorageRecord(nk, ACTIVITY_COLLECTION, ACTIVITY_KEY, ctx.userId); removePlayerStats(nk, playerRecord, Date.now()); nk.accountDeleteId(ctx.userId, false); return JSON.stringify({accepted:true}); } function normalizeRoomCode(value) { return String(value || "").toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 6); } function generateRoomCode() { var alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789", code = ""; for (var i = 0; i < 6; i++) code += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); return code; } function privateRoomRecord(nk, code) { var records = nk.storageRead([{collection:ROOM_COLLECTION,key:code,userId:SYSTEM_USER_ID}]); return records.length ? records[0] : null; } function deletePrivateRoomRecord(nk, code) { if (code) nk.storageDelete([{collection:ROOM_COLLECTION,key:code,userId:SYSTEM_USER_ID}]); } function rpcCreatePrivateRoom(ctx, logger, nk, payload) { if (!ctx.userId) throw new Error("authentication required"); var request; try { request = JSON.parse(payload || "{}"); } catch (error) { request = {}; } var difficulty = Math.max(0, Math.min(2, Math.floor(Number(request.difficulty || 1)))); var code = ""; for (var attempt = 0; attempt < 24; attempt++) { var candidate = generateRoomCode(); if (!privateRoomRecord(nk, candidate)) { code = candidate; break; } } if (!code) throw new Error("room code allocation failed"); var matchId = nk.matchCreate(MATCH_MODULE, {difficulty:difficulty,mode:"private",roomCode:code,hostUserId:ctx.userId}); nk.storageWrite([{collection:ROOM_COLLECTION,key:code,userId:SYSTEM_USER_ID,value:{match_id:matchId,host_user_id:ctx.userId,created_at:Date.now()},permissionRead:0,permissionWrite:0}]); return JSON.stringify({created:true,room_code:code,match_id:matchId}); } function rpcJoinPrivateRoom(ctx, logger, nk, payload) { if (!ctx.userId) throw new Error("authentication required"); var request; try { request = JSON.parse(payload || "{}"); } catch (error) { request = {}; } var code = normalizeRoomCode(request.room_code); if (code.length !== 6) return JSON.stringify({found:false,error:"invalid_room_code"}); var record = privateRoomRecord(nk, code); if (!record || !record.value || !record.value.match_id) return JSON.stringify({found:false,error:"room_not_found"}); return JSON.stringify({found:true,room_code:code,match_id:String(record.value.match_id)}); } function matchmakerMatched(ctx, logger, nk, matches) { var difficulty = 1; if (matches.length && matches[0].properties && matches[0].properties.difficulty !== undefined) difficulty = Math.max(0,Math.min(2,Math.floor(Number(matches[0].properties.difficulty)))); return nk.matchCreate(MATCH_MODULE,{matchedUsers:matches.length,difficulty:difficulty}); } function initialState(params) { var difficulty = Math.max(0,Math.min(2,Math.floor(Number(params.difficulty||1)))); var credits = DIFFICULTIES[difficulty].startCredits; return {presences:{},slots:{},players:{},ready:{},started:false,pausedForReconnect:false,reconnectDeadlineTick:0,gameOver:false,difficulty:difficulty,mode:String(params.mode||"quick"),roomCode:normalizeRoomCode(params.roomCode),hostUserId:String(params.hostUserId||""),emptyTicks:0,credits:[credits,credits],reactorLives:30,wave:0,waveActive:false,spawnRemaining:0,waveSchedule:[],spawnTimer:0,nextSpawnRoute:0,nextEnemyId:1,nextTowerId:1,towers:[],enemies:[],lastSnapshotTick:0}; } function matchLabel(state) { return state.mode === "private" ? state.roomCode : JSON.stringify({game:"void_bastion",mode:state.mode,open:!state.started&&!state.gameOver}); } function matchInit(ctx,logger,nk,params) { var state=initialState(params||{}); return {state:state,tickRate:TICK_RATE,label:matchLabel(state)}; } function matchJoinAttempt(ctx,logger,nk,dispatcher,tick,state,presence,metadata) { return {state:state,accept:state.slots[presence.userId]!==undefined||Object.keys(state.slots).length<2}; } function firstFreeSlot(state) { var used={},ids=Object.keys(state.slots); for (var i=0;i0?((px-ax)*abx+(py-ay)*aby)/len:0; t=Math.max(0,Math.min(1,t)); return Math.sqrt(distanceSquared(px,py,ax+abx*t,ay+aby*t)); } function validBuildPosition(state,slot,x,y) { var sx=Math.floor(x/GRID)*GRID+GRID*0.5,sy=Math.floor(y/GRID)*GRID+GRID*0.5; if (Math.abs(sx-x)>1||Math.abs(sy-y)>1||yPLAYFIELD_BOTTOM) return false; if (slot===0&&(x<120||x>=MIRROR_X-GRID*0.5)) return false; if (slot===1&&(x<=MIRROR_X+GRID*0.5||x>WORLD_WIDTH-120)) return false; if (distanceSquared(x,y,REACTOR_X,REACTOR_Y)<=REACTOR_RADIUS*REACTOR_RADIUS) return false; for (var r=0;r=TOWERS.length||!validBuildPosition(state,slot,x,y)||state.credits[slot]=3) return false; var cost=upgradeCost(state.towers[i]); if (state.credits[slot]=10?8:5; var offset=difficulty===2?2:(difficulty===1?1:0); if (wave>=10&&roll>=0.96) return 8; if (wave>=8&&roll>=0.84&&roll<0.94) return 7; if (wave>=3&&roll>=0.52&&roll<0.64) return 6; if (wave>=10-offset&&roll>=0.94) return 5; if (wave>=7-offset&&roll>=0.80) return 4; if (wave>=4-offset&&roll>=0.65) return 3; if (wave>=7&&roll>=0.45) return 2; if (wave>=3&&roll>=0.28) return 1; return 0; } function wavePopulation(state,wave) { var scale=1+Math.max(0,wave-3)*0.08; if (state.difficulty===2) scale+=Math.max(0,wave-4)*0.025; return Math.ceil((7+wave*2)*DIFFICULTIES[state.difficulty].count*scale)*2; } function buildWaveSchedule(state,wave,count) { var schedule=[],bossCount=wave>0&&wave%5===0?2:0; for (var i=0;i=count-bossCount; var roll=((i+1)*0.61803398875+wave*0.38196601125)%1; schedule.push({type:enemyTypeForWave(wave,roll,state.difficulty,boss),boss:boss}); } return schedule; } function startWave(state) { if (state.waveActive||state.gameOver||!state.started) return false; state.wave+=1; state.waveSchedule=buildWaveSchedule(state,state.wave,wavePopulation(state,state.wave)); state.spawnRemaining=state.waveSchedule.length; state.spawnTimer=0.1; state.waveActive=true; return true; } function spawnEnemy(state) { var entry=state.waveSchedule.length?state.waveSchedule.shift():{type:0,boss:false}; var boss=entry.boss,type=entry.type; var base=ENEMIES[type],d=DIFFICULTIES[state.difficulty]; var scale=(1+Math.max(0,state.wave-1)*0.20+Math.max(0,state.wave-3)*0.09+((state.difficulty===2)?Math.max(0,state.wave-5)*0.055:0))*(boss?3.25:1)*d.hp; var routeId=state.nextSpawnRoute; state.nextSpawnRoute=(state.nextSpawnRoute+1)%2; var route=PATHS[routeId]; var hasShield=type===4||(state.wave>=4&&Math.random()=0;i--) { var e=state.enemies[i],route=PATHS[e.route_id],target=route[e.path_index],dx=target[0]-e.x,dy=target[1]-e.y,dist=Math.sqrt(dx*dx+dy*dy); e.angle=Math.atan2(dy,dx); if (dist<=Math.max(9,e.speed*delta)) { e.x=target[0]; e.y=target[1]; e.path_index+=1; if (e.path_index>=route.length) { state.reactorLives=Math.max(0,state.reactorLives-(ENEMIES[e.type].breachDamage+(e.boss?4:0))); state.enemies.splice(i,1); if (state.reactorLives<=0) { state.gameOver=true; state.waveActive=false; state.spawnRemaining=0; } } } else { e.x+=dx/dist*e.speed*delta; e.y+=dy/dist*e.speed*delta; } } } function enemyProgress(e) { return e.path_index*100000-distanceSquared(e.x,e.y,REACTOR_X,REACTOR_Y); } function updateTowers(state,delta) { for (var i=0;ibest)) { best=p; bestShield=shieldPriority; targetIndex=j; } } } if (targetIndex<0) continue; var target=state.enemies[targetIndex]; t.angle=Math.atan2(target.y-t.y,target.x-t.x)+([0,3,5].indexOf(t.type)>=0?Math.PI:0); if (t.cooldown>0) continue; var damage=towerDamage(t)*(TOWERS[t.type].targetClass==="both"?TOWERS[t.type].bothScale:1); if (t.type===5) { var aimLength=Math.sqrt(distanceSquared(t.x,t.y,target.x,target.y)),dx=(target.x-t.x)/aimLength,dy=(target.y-t.y)/aimLength,rangeSquared=range*range; for (var railIndex=state.enemies.length-1;railIndex>=0;railIndex--) { var railTarget=state.enemies[railIndex]; if (distanceSquared(t.x,t.y,railTarget.x,railTarget.y)>rangeSquared) continue; var offsetX=railTarget.x-t.x,offsetY=railTarget.y-t.y,along=offsetX*dx+offsetY*dy,across=Math.abs(offsetX*dy-offsetY*dx); if (along>=0&&along<=range&&across<=18) { applyDamageToEnemy(railTarget,damage,t.type); if (railTarget.hp<=0) removeEnemy(state,railIndex,t.owner); } } } else if (t.type===2||t.type===3||t.type===4) { var radius=t.type===2?76:(t.type===3?92:118),splash=t.type===2?0.45:(t.type===3?0.62:1); var tx=target.x,ty=target.y,targetId=target.id; for (var k=state.enemies.length-1;k>=0;k--) if ((TOWERS[t.type].targetClass==="both"||TOWERS[t.type].targetClass===ENEMIES[state.enemies[k].type].category)&&distanceSquared(state.enemies[k].x,state.enemies[k].y,tx,ty)<=radius*radius) { applyDamageToEnemy(state.enemies[k],state.enemies[k].id===targetId?damage:damage*splash,t.type); if (state.enemies[k].hp<=0) removeEnemy(state,k,t.owner); } } else { applyDamageToEnemy(target,damage,t.type); if (target.hp<=0) removeEnemy(state,targetIndex,t.owner); } t.cooldown=towerCooldown(t); } } function updateWave(state,delta,dispatcher) { if (!state.waveActive||state.gameOver) return; if (state.spawnRemaining>0) { state.spawnTimer-=delta; if (state.spawnTimer<=0) { spawnEnemy(state); state.spawnRemaining-=1; state.spawnTimer=Math.max(0.24,(0.82-state.wave*0.025)*DIFFICULTIES[state.difficulty].spawnDelay); } } else if (!state.enemies.length) { state.waveActive=false; var bonus=Math.floor((55+state.wave*8)*DIFFICULTIES[state.difficulty].income); state.credits[0]+=bonus; state.credits[1]+=bonus; broadcastEvent(dispatcher,"wave_complete",{wave:state.wave,bonus:bonus}); } } function handleCommand(nk,dispatcher,state,message) { var slot=state.slots[message.sender.userId]; if (slot===undefined) return; var payload=parseMessage(nk,message); if (payload===null) return; var changed=false; if (message.opCode===OP_BUILD) changed=handleBuild(state,slot,payload); else if (message.opCode===OP_UPGRADE) changed=handleUpgrade(state,slot,payload); else if (message.opCode===OP_SELL) changed=handleSell(state,slot,payload); else if (message.opCode===OP_START_WAVE) changed=startWave(state); else if (message.opCode===OP_READY&&!state.started) { state.ready[message.sender.userId]=payload.ready!==false; var requestedName=normalizeDisplayName(payload.name||message.sender.username||"PILOT"); var name=validateDisplayName(requestedName) ? "PILOT" : requestedName; state.players[message.sender.userId]={name:name||"PILOT"}; var ids=Object.keys(state.slots),allReady=ids.length===2&&Object.keys(state.presences).length===2; for (var i=0;i= TICK_RATE * 60) { deletePrivateRoomRecord(nk,state.roomCode); return null; } } else state.emptyTicks = 0; for (var i=0;i=state.reconnectDeadlineTick) { state.gameOver=true; state.pausedForReconnect=false; state.waveActive=false; broadcastEvent(dispatcher,"partner_timeout",{}); } if (state.started&&!state.pausedForReconnect&&!state.gameOver) { var delta=1/TICK_RATE; updateWave(state,delta,dispatcher); updateEnemies(state,delta); updateTowers(state,delta); } if (tick-state.lastSnapshotTick>=SNAPSHOT_TICKS||messages.length) broadcastSnapshot(dispatcher,state,tick); return {state:state}; } function matchTerminate(ctx,logger,nk,dispatcher,tick,state,graceSeconds) { deletePrivateRoomRecord(nk,state.roomCode); broadcastEvent(dispatcher,"server_shutdown",{grace:graceSeconds}); return {state:state}; } function matchSignal(ctx,logger,nk,dispatcher,tick,state,data) { return {state:state,data:JSON.stringify(snapshot(state,tick))}; } function InitModule(ctx,logger,nk,initializer) { for (var mode=0;mode