Add private multiplayer room codes
This commit is contained in:
1 parent
7362ca0f02
commit
19b1a67773
8 files changed
+341
-24
No files matched your search
@@ -3,6 +3,8 @@ 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;
|
||||
@@ -46,6 +48,47 @@ function rpcSubmitRun(ctx, logger, nk, payload) {
|
||||
return JSON.stringify({accepted:true,leaderboard_id:id,rank:record.rank,score:Number(record.score),wave:wave});
|
||||
}
|
||||
|
||||
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))));
|
||||
@@ -54,9 +97,10 @@ function matchmakerMatched(ctx, logger, nk, matches) {
|
||||
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:{},started:false,gameOver:false,difficulty:difficulty,credits:[credits,credits],reactorLives:30,wave:0,waveActive:false,spawnRemaining:0,spawnTimer:0,nextSpawnRoute:0,nextEnemyId:1,nextTowerId:1,towers:[],enemies:[],lastSnapshotTick:0};
|
||||
return {presences:{},slots:{},started:false,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 matchInit(ctx,logger,nk,params) { return {state:initialState(params||{}),tickRate:TICK_RATE,label:JSON.stringify({game:"void_bastion",mode:"coop",open:true})}; }
|
||||
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:Object.keys(state.presences).length<2}; }
|
||||
function firstFreeSlot(state) {
|
||||
var used={},ids=Object.keys(state.slots);
|
||||
@@ -71,13 +115,15 @@ function broadcastEvent(dispatcher,type,data) { var payload=data||{}; payload.ty
|
||||
function matchJoin(ctx,logger,nk,dispatcher,tick,state,presences) {
|
||||
for (var i=0;i<presences.length;i++) { var p=presences[i]; state.presences[p.userId]=p; if (state.slots[p.userId]===undefined) state.slots[p.userId]=firstFreeSlot(state); }
|
||||
state.started=Object.keys(state.presences).length===2;
|
||||
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.started=false; broadcastEvent(dispatcher,"partner_left",{}); broadcastSnapshot(dispatcher,state,tick); return {state:state};
|
||||
state.started=false; dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"partner_left",{}); 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); }
|
||||
@@ -174,18 +220,24 @@ function handleCommand(nk,dispatcher,state,message) {
|
||||
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.started&&!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) { broadcastEvent(dispatcher,"server_shutdown",{grace:graceSeconds}); 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("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);
|
||||
|
||||
Reference in new issue
Block a user