298 lines
21 KiB
JavaScript
298 lines
21 KiB
JavaScript
var GAME_VERSION = "0.22";
|
|
var MAP_COUNT = 3;
|
|
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 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},
|
|
{cost:180,damage:12,range:165,cooldown:0.18},
|
|
{cost:250,damage:65,range:260,cooldown:1.40}
|
|
];
|
|
var ENEMIES = [{hp:70,speed:105,reward:18},{hp:185,speed:72,reward:34},{hp:520,speed:43,reward:78}];
|
|
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 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 metadata = {map:mapIndex,difficulty:difficulty,mode:mode,wave:wave,clientVersion:String(run.client_version||"unknown"),submittedAt:Date.now()};
|
|
var record = nk.leaderboardRecordWrite(id,ctx.userId,ctx.username||"Pilot",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");
|
|
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,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;i<ids.length;i++) used[state.slots[ids[i]]]=true;
|
|
return used[0] ? 1 : 0;
|
|
}
|
|
function snapshot(state,tick) {
|
|
var lobby=[null,null],ids=Object.keys(state.slots);
|
|
for (var i=0;i<ids.length;i++) {
|
|
var id=ids[i],slot=state.slots[id],player=state.players[id]||{};
|
|
lobby[slot]={user_id:id,slot:slot,name:String(player.name||"PILOT"),ready:!!state.ready[id],connected:state.presences[id]!==undefined};
|
|
}
|
|
return {version:GAME_VERSION,tick:tick,started:state.started,connection_interrupted:state.pausedForReconnect,reconnect_seconds:state.pausedForReconnect?Math.max(0,Math.ceil((state.reconnectDeadlineTick-tick)/TICK_RATE)):0,game_over:state.gameOver,difficulty:state.difficulty,slots:state.slots,players:lobby,credits:state.credits,reactor_lives:state.reactorLives,wave:state.wave,wave_active:state.waveActive,enemies_to_spawn:state.spawnRemaining,towers:state.towers,enemies:state.enemies};
|
|
}
|
|
function broadcastSnapshot(dispatcher,state,tick) { dispatcher.broadcastMessage(OP_SNAPSHOT,JSON.stringify(snapshot(state,tick)),null,null,true); state.lastSnapshotTick=tick; }
|
|
function broadcastEvent(dispatcher,type,data) { var payload=data||{}; payload.type=type; dispatcher.broadcastMessage(OP_EVENT,JSON.stringify(payload),null,null,true); }
|
|
function matchJoin(ctx,logger,nk,dispatcher,tick,state,presences) {
|
|
var rejoined=false;
|
|
for (var i=0;i<presences.length;i++) {
|
|
var p=presences[i];
|
|
if (state.slots[p.userId]!==undefined) rejoined=true;
|
|
state.presences[p.userId]=p;
|
|
if (state.slots[p.userId]===undefined) state.slots[p.userId]=firstFreeSlot(state);
|
|
if (state.ready[p.userId]===undefined) state.ready[p.userId]=false;
|
|
state.players[p.userId]={name:String(p.username||"PILOT").substring(0,24)};
|
|
}
|
|
if (state.started&&Object.keys(state.presences).length===2) { state.pausedForReconnect=false; state.reconnectDeadlineTick=0; if (rejoined) broadcastEvent(dispatcher,"partner_rejoined",{}); }
|
|
state.emptyTicks=0;
|
|
dispatcher.matchLabelUpdate(matchLabel(state));
|
|
broadcastEvent(dispatcher,"players",{count:Object.keys(state.presences).length,started:state.started,slots:state.slots});
|
|
broadcastSnapshot(dispatcher,state,tick);
|
|
return {state:state};
|
|
}
|
|
function matchLeave(ctx,logger,nk,dispatcher,tick,state,presences) {
|
|
for (var i=0;i<presences.length;i++) { delete state.presences[presences[i].userId]; state.ready[presences[i].userId]=false; }
|
|
if (state.started) { state.pausedForReconnect=true; state.reconnectDeadlineTick=tick+TICK_RATE*60; }
|
|
dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"partner_left",{reconnect_seconds:60}); broadcastSnapshot(dispatcher,state,tick); return {state:state};
|
|
}
|
|
function parseMessage(nk,message) { try { return JSON.parse(nk.binaryToString(message.data)||"{}"); } catch (error) { return null; } }
|
|
function towerDamage(t) { return TOWERS[t.type].damage*(1+(t.level-1)*0.52); }
|
|
function towerRange(t) { return TOWERS[t.type].range*(1+(t.level-1)*0.08); }
|
|
function towerCooldown(t) { return TOWERS[t.type].cooldown/(1+(t.level-1)*0.12); }
|
|
function upgradeCost(t) { return Math.floor(TOWERS[t.type].cost*(0.72+t.level*0.28)); }
|
|
function distanceSquared(ax,ay,bx,by) { var dx=ax-bx,dy=ay-by; return dx*dx+dy*dy; }
|
|
function distanceToSegment(px,py,ax,ay,bx,by) {
|
|
var abx=bx-ax,aby=by-ay,len=abx*abx+aby*aby,t=len>0?((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||y<PLAYFIELD_TOP||y>PLAYFIELD_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<PATHS.length;r++) for (var i=0;i<PATHS[r].length-1;i++) if (distanceToSegment(x,y,PATHS[r][i][0],PATHS[r][i][1],PATHS[r][i+1][0],PATHS[r][i+1][1])<GRID*0.62) return false;
|
|
for (var j=0;j<state.towers.length;j++) if (distanceSquared(x,y,state.towers[j].x,state.towers[j].y)<GRID*GRID*0.52) return false;
|
|
return true;
|
|
}
|
|
function handleBuild(state,slot,payload) {
|
|
var type=Math.floor(Number(payload.tower_type)),x=Number(payload.x),y=Number(payload.y);
|
|
if (!isFinite(x)||!isFinite(y)||type<0||type>=TOWERS.length||!validBuildPosition(state,slot,x,y)||state.credits[slot]<TOWERS[type].cost) return false;
|
|
state.credits[slot]-=TOWERS[type].cost;
|
|
state.towers.push({id:state.nextTowerId++,x:x,y:y,type:type,level:1,owner:slot,cooldown:0.15,angle:slot===0?0:Math.PI}); return true;
|
|
}
|
|
function ownedTowerIndex(state,slot,id) { for (var i=0;i<state.towers.length;i++) if (state.towers[i].id===id&&state.towers[i].owner===slot) return i; return -1; }
|
|
function handleUpgrade(state,slot,payload) {
|
|
var i=ownedTowerIndex(state,slot,Math.floor(Number(payload.tower_id)));
|
|
if (i<0||state.towers[i].level>=3) return false;
|
|
var cost=upgradeCost(state.towers[i]); if (state.credits[slot]<cost) return false;
|
|
state.credits[slot]-=cost; state.towers[i].level+=1; return true;
|
|
}
|
|
function handleSell(state,slot,payload) {
|
|
var i=ownedTowerIndex(state,slot,Math.floor(Number(payload.tower_id))); if (i<0) return false;
|
|
var tower=state.towers[i]; state.credits[slot]+=Math.floor(TOWERS[tower.type].cost*(0.55+tower.level*0.12)); state.towers.splice(i,1); return true;
|
|
}
|
|
function startWave(state) {
|
|
if (state.waveActive||state.gameOver||!state.started) return false;
|
|
state.wave+=1; state.spawnRemaining=Math.ceil((7+state.wave*2)*DIFFICULTIES[state.difficulty].count)*2; state.spawnTimer=0.1; state.waveActive=true; return true;
|
|
}
|
|
function spawnEnemy(state) {
|
|
var boss=state.wave>0&&state.wave%5===0&&state.spawnRemaining<=2,roll=Math.random();
|
|
var type=boss?2:(state.wave>=7&&roll>0.82?2:(state.wave>=3&&roll>0.53?1:0));
|
|
var base=ENEMIES[type],d=DIFFICULTIES[state.difficulty],scale=(1+Math.max(0,state.wave-1)*0.115)*(boss?3.25:1)*d.hp;
|
|
var routeId=state.nextSpawnRoute; state.nextSpawnRoute=(state.nextSpawnRoute+1)%2; var route=PATHS[routeId];
|
|
state.enemies.push({id:state.nextEnemyId++,type:type,x:route[0][0],y:route[0][1],path_index:1,route_id:routeId,hp:base.hp*scale,max_hp:base.hp*scale,speed:base.speed*(1+state.wave*0.012)*(boss?0.82:1)*d.speed,reward:Math.floor((base.reward+state.wave)*(boss?4:1)*d.income),angle:0,boss:boss});
|
|
}
|
|
function removeEnemy(state,index,owner) { state.credits[owner]+=state.enemies[index].reward; state.enemies.splice(index,1); }
|
|
function updateEnemies(state,delta) {
|
|
for (var i=state.enemies.length-1;i>=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-(1+e.type+(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;i<state.towers.length;i++) {
|
|
var t=state.towers[i]; t.cooldown-=delta; var range=towerRange(t),targetIndex=-1,best=-1;
|
|
for (var j=0;j<state.enemies.length;j++) if (distanceSquared(t.x,t.y,state.enemies[j].x,state.enemies[j].y)<=range*range) { var p=enemyProgress(state.enemies[j]); if (p>best) { best=p; targetIndex=j; } }
|
|
if (targetIndex<0) continue;
|
|
var target=state.enemies[targetIndex]; t.angle=Math.atan2(target.y-t.y,target.x-t.x)+(t.type===0?Math.PI:0);
|
|
if (t.cooldown>0) continue;
|
|
var damage=towerDamage(t);
|
|
if (t.type===2) {
|
|
var tx=target.x,ty=target.y,targetId=target.id;
|
|
for (var k=state.enemies.length-1;k>=0;k--) if (distanceSquared(state.enemies[k].x,state.enemies[k].y,tx,ty)<=76*76) { state.enemies[k].hp-=state.enemies[k].id===targetId?damage:damage*0.45; if (state.enemies[k].hp<=0) removeEnemy(state,k,t.owner); }
|
|
} else { target.hp-=damage; 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<ids.length;i++) if (!state.ready[ids[i]]||state.presences[ids[i]]===undefined) allReady=false;
|
|
if (allReady) { state.started=true; state.pausedForReconnect=false; dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"match_started",{}); }
|
|
changed=true;
|
|
}
|
|
if (!changed) broadcastEvent(dispatcher,"command_rejected",{op_code:message.opCode,player:slot});
|
|
}
|
|
function matchLoop(ctx,logger,nk,dispatcher,tick,state,messages) {
|
|
if (!Object.keys(state.presences).length) {
|
|
state.emptyTicks += 1;
|
|
if (state.emptyTicks >= TICK_RATE * 60) { deletePrivateRoomRecord(nk,state.roomCode); return null; }
|
|
} else state.emptyTicks = 0;
|
|
for (var i=0;i<messages.length;i++) handleCommand(nk,dispatcher,state,messages[i]);
|
|
if (state.pausedForReconnect&&tick>=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<VALID_MODES.length;mode++) for (var map=0;map<MAP_COUNT;map++) for (var difficulty=0;difficulty<DIFFICULTY_COUNT;difficulty++) nk.leaderboardCreate(leaderboardId(map,difficulty,VALID_MODES[mode]),true,"desc","best",null,{game:"void_bastion"});
|
|
initializer.registerRpc("server_info",rpcServerInfo);
|
|
initializer.registerRpc("submit_run",rpcSubmitRun);
|
|
initializer.registerRpc("update_display_name",rpcUpdateDisplayName);
|
|
initializer.registerRpc("delete_account",rpcDeleteAccount);
|
|
initializer.registerRpc("create_private_room",rpcCreatePrivateRoom);
|
|
initializer.registerRpc("join_private_room",rpcJoinPrivateRoom);
|
|
initializer.registerMatchmakerMatched(matchmakerMatched);
|
|
initializer.registerMatch(MATCH_MODULE,{matchInit:matchInit,matchJoinAttempt:matchJoinAttempt,matchJoin:matchJoin,matchLeave:matchLeave,matchLoop:matchLoop,matchTerminate:matchTerminate,matchSignal:matchSignal});
|
|
logger.info("Void Bastion online runtime %s loaded",GAME_VERSION);
|
|
}
|