connectbase-client 1.12.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/MIGRATION-v2.md +149 -0
- package/README.md +23 -0
- package/dist/connect-base.umd.js +3 -3
- package/dist/index.d.mts +118 -98
- package/dist/index.d.ts +118 -98
- package/dist/index.js +161 -555
- package/dist/index.mjs +161 -555
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1651,41 +1651,6 @@ var DatabaseAPI = class {
|
|
|
1651
1651
|
}
|
|
1652
1652
|
};
|
|
1653
1653
|
}
|
|
1654
|
-
/**
|
|
1655
|
-
* 프레즌스 상태 설정
|
|
1656
|
-
*/
|
|
1657
|
-
setPresence(status, device, metadata) {
|
|
1658
|
-
if (this.realtimeState !== "connected") return;
|
|
1659
|
-
this.sendRealtimeMessage({
|
|
1660
|
-
type: "presence_set",
|
|
1661
|
-
request_id: this.generateRequestId(),
|
|
1662
|
-
status,
|
|
1663
|
-
device,
|
|
1664
|
-
metadata
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
/**
|
|
1668
|
-
* 프레즌스 구독 (다른 사용자의 온라인 상태 감시)
|
|
1669
|
-
*/
|
|
1670
|
-
subscribePresence(userIds, onPresence) {
|
|
1671
|
-
if (this.realtimeState !== "connected") return;
|
|
1672
|
-
this.realtimeHandlers.set("__presence__", {
|
|
1673
|
-
onSnapshot: (docs) => {
|
|
1674
|
-
const states = {};
|
|
1675
|
-
for (const doc of docs) {
|
|
1676
|
-
if (doc.data) {
|
|
1677
|
-
states[doc.id] = doc.data;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
onPresence(states);
|
|
1681
|
-
}
|
|
1682
|
-
});
|
|
1683
|
-
this.sendRealtimeMessage({
|
|
1684
|
-
type: "presence_subscribe",
|
|
1685
|
-
request_id: this.generateRequestId(),
|
|
1686
|
-
user_ids: userIds
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
1654
|
/**
|
|
1690
1655
|
* 실시간 연결 상태 확인
|
|
1691
1656
|
*/
|
|
@@ -1727,7 +1692,7 @@ var DatabaseAPI = class {
|
|
|
1727
1692
|
this.setRealtimeState("connecting");
|
|
1728
1693
|
const baseUrl = this.realtimeOptions.dataServerUrl || this.http.getBaseUrl();
|
|
1729
1694
|
const wsUrl = baseUrl.replace(/^http/, "ws");
|
|
1730
|
-
const url = `${wsUrl}/v1/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
1695
|
+
const url = `${wsUrl}/v1/database/realtime/ws?access_token=${encodeURIComponent(this.realtimeOptions.accessToken)}`;
|
|
1731
1696
|
return new Promise((resolve, reject) => {
|
|
1732
1697
|
try {
|
|
1733
1698
|
this.realtimeWs = new WebSocket(url);
|
|
@@ -6647,495 +6612,6 @@ var GameAPI = class {
|
|
|
6647
6612
|
async deleteRoom(_roomId) {
|
|
6648
6613
|
throw new Error("cb.game.deleteRoom is not yet publicly available \u2014 use the admin console or request a backend public route.");
|
|
6649
6614
|
}
|
|
6650
|
-
// ==================== Matchmaking ====================
|
|
6651
|
-
/**
|
|
6652
|
-
* 매치메이킹 큐에 참가
|
|
6653
|
-
*/
|
|
6654
|
-
async joinQueue(req) {
|
|
6655
|
-
const id = this.appId || "";
|
|
6656
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/matchmaking/queue`, {
|
|
6657
|
-
method: "POST",
|
|
6658
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6659
|
-
body: JSON.stringify({
|
|
6660
|
-
game_type: req.gameType,
|
|
6661
|
-
player_id: req.playerId,
|
|
6662
|
-
rating: req.rating,
|
|
6663
|
-
region: req.region,
|
|
6664
|
-
mode: req.mode,
|
|
6665
|
-
party_members: req.partyMembers,
|
|
6666
|
-
metadata: req.metadata
|
|
6667
|
-
})
|
|
6668
|
-
});
|
|
6669
|
-
if (!response.ok) {
|
|
6670
|
-
const err = await response.json().catch(() => ({}));
|
|
6671
|
-
throw new Error(err.error || `Failed to join queue: ${response.statusText}`);
|
|
6672
|
-
}
|
|
6673
|
-
const data = await response.json();
|
|
6674
|
-
return {
|
|
6675
|
-
ticketId: data.ticket_id,
|
|
6676
|
-
gameType: data.game_type,
|
|
6677
|
-
playerId: data.player_id,
|
|
6678
|
-
status: data.status,
|
|
6679
|
-
createdAt: data.created_at
|
|
6680
|
-
};
|
|
6681
|
-
}
|
|
6682
|
-
/**
|
|
6683
|
-
* 매치메이킹 큐에서 탈퇴
|
|
6684
|
-
*/
|
|
6685
|
-
async leaveQueue(ticketId) {
|
|
6686
|
-
const id = this.appId || "";
|
|
6687
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/matchmaking/queue`, {
|
|
6688
|
-
method: "DELETE",
|
|
6689
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6690
|
-
body: JSON.stringify({ ticket_id: ticketId })
|
|
6691
|
-
});
|
|
6692
|
-
if (!response.ok) {
|
|
6693
|
-
const err = await response.json().catch(() => ({}));
|
|
6694
|
-
throw new Error(err.error || `Failed to leave queue: ${response.statusText}`);
|
|
6695
|
-
}
|
|
6696
|
-
}
|
|
6697
|
-
/**
|
|
6698
|
-
* 매치메이킹 상태 조회
|
|
6699
|
-
*/
|
|
6700
|
-
async getMatchStatus(params) {
|
|
6701
|
-
const id = this.appId || "";
|
|
6702
|
-
const searchParams = new URLSearchParams();
|
|
6703
|
-
if (params.ticketId) searchParams.set("ticket_id", params.ticketId);
|
|
6704
|
-
if (params.playerId) searchParams.set("player_id", params.playerId);
|
|
6705
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/matchmaking/status?${searchParams}`, {
|
|
6706
|
-
headers: this.getHeaders()
|
|
6707
|
-
});
|
|
6708
|
-
if (!response.ok) {
|
|
6709
|
-
const err = await response.json().catch(() => ({}));
|
|
6710
|
-
throw new Error(err.error || `Failed to get match status: ${response.statusText}`);
|
|
6711
|
-
}
|
|
6712
|
-
const data = await response.json();
|
|
6713
|
-
return {
|
|
6714
|
-
ticketId: data.ticket_id,
|
|
6715
|
-
gameType: data.game_type,
|
|
6716
|
-
playerId: "",
|
|
6717
|
-
status: data.status,
|
|
6718
|
-
createdAt: "",
|
|
6719
|
-
waitTime: data.wait_time,
|
|
6720
|
-
matchId: data.match_id,
|
|
6721
|
-
roomId: data.room_id
|
|
6722
|
-
};
|
|
6723
|
-
}
|
|
6724
|
-
// ==================== Lobbies ====================
|
|
6725
|
-
/**
|
|
6726
|
-
* 로비 목록 조회
|
|
6727
|
-
*/
|
|
6728
|
-
async listLobbies() {
|
|
6729
|
-
const id = this.appId || "";
|
|
6730
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies`, {
|
|
6731
|
-
headers: this.getHeaders()
|
|
6732
|
-
});
|
|
6733
|
-
if (!response.ok) {
|
|
6734
|
-
throw new Error(`Failed to list lobbies: ${response.statusText}`);
|
|
6735
|
-
}
|
|
6736
|
-
const data = await response.json();
|
|
6737
|
-
return (data.lobbies || []).map((l) => ({
|
|
6738
|
-
id: l.id,
|
|
6739
|
-
name: l.name,
|
|
6740
|
-
hostId: l.host_id,
|
|
6741
|
-
gameType: l.game_type,
|
|
6742
|
-
playerCount: l.player_count,
|
|
6743
|
-
maxPlayers: l.max_players,
|
|
6744
|
-
hasPassword: l.has_password,
|
|
6745
|
-
visibility: l.visibility,
|
|
6746
|
-
region: l.region,
|
|
6747
|
-
settings: l.settings,
|
|
6748
|
-
tags: l.tags,
|
|
6749
|
-
status: l.status,
|
|
6750
|
-
createdAt: l.created_at
|
|
6751
|
-
}));
|
|
6752
|
-
}
|
|
6753
|
-
/**
|
|
6754
|
-
* 로비 생성
|
|
6755
|
-
*/
|
|
6756
|
-
async createLobby(req) {
|
|
6757
|
-
const id = this.appId || "";
|
|
6758
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies`, {
|
|
6759
|
-
method: "POST",
|
|
6760
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6761
|
-
body: JSON.stringify({
|
|
6762
|
-
player_id: req.playerId,
|
|
6763
|
-
display_name: req.displayName,
|
|
6764
|
-
name: req.name,
|
|
6765
|
-
game_type: req.gameType,
|
|
6766
|
-
password: req.password,
|
|
6767
|
-
max_players: req.maxPlayers,
|
|
6768
|
-
visibility: req.visibility,
|
|
6769
|
-
region: req.region,
|
|
6770
|
-
settings: req.settings,
|
|
6771
|
-
tags: req.tags
|
|
6772
|
-
})
|
|
6773
|
-
});
|
|
6774
|
-
if (!response.ok) {
|
|
6775
|
-
const err = await response.json().catch(() => ({}));
|
|
6776
|
-
throw new Error(err.error || `Failed to create lobby: ${response.statusText}`);
|
|
6777
|
-
}
|
|
6778
|
-
const data = await response.json();
|
|
6779
|
-
return {
|
|
6780
|
-
id: data.id,
|
|
6781
|
-
name: data.name,
|
|
6782
|
-
hostId: data.host_id,
|
|
6783
|
-
gameType: data.game_type,
|
|
6784
|
-
playerCount: 1,
|
|
6785
|
-
maxPlayers: data.max_players,
|
|
6786
|
-
hasPassword: !!req.password,
|
|
6787
|
-
visibility: data.visibility,
|
|
6788
|
-
region: data.region,
|
|
6789
|
-
settings: data.settings,
|
|
6790
|
-
tags: data.tags,
|
|
6791
|
-
status: data.status,
|
|
6792
|
-
createdAt: data.created_at
|
|
6793
|
-
};
|
|
6794
|
-
}
|
|
6795
|
-
/**
|
|
6796
|
-
* 로비 상세 조회
|
|
6797
|
-
*/
|
|
6798
|
-
async getLobby(lobbyId) {
|
|
6799
|
-
const id = this.appId || "";
|
|
6800
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}`, {
|
|
6801
|
-
headers: this.getHeaders()
|
|
6802
|
-
});
|
|
6803
|
-
if (!response.ok) {
|
|
6804
|
-
throw new Error(`Failed to get lobby: ${response.statusText}`);
|
|
6805
|
-
}
|
|
6806
|
-
const data = await response.json();
|
|
6807
|
-
return {
|
|
6808
|
-
id: data.id,
|
|
6809
|
-
name: data.name,
|
|
6810
|
-
hostId: data.host_id,
|
|
6811
|
-
gameType: data.game_type,
|
|
6812
|
-
playerCount: data.player_count,
|
|
6813
|
-
maxPlayers: data.max_players,
|
|
6814
|
-
hasPassword: data.has_password,
|
|
6815
|
-
visibility: data.visibility,
|
|
6816
|
-
region: data.region,
|
|
6817
|
-
settings: data.settings,
|
|
6818
|
-
tags: data.tags,
|
|
6819
|
-
status: data.status,
|
|
6820
|
-
roomId: data.room_id,
|
|
6821
|
-
members: (data.members || []).map((m) => ({
|
|
6822
|
-
playerId: m.player_id,
|
|
6823
|
-
displayName: m.display_name,
|
|
6824
|
-
role: m.role,
|
|
6825
|
-
team: m.team,
|
|
6826
|
-
ready: m.ready,
|
|
6827
|
-
slot: m.slot,
|
|
6828
|
-
joinedAt: m.joined_at
|
|
6829
|
-
})),
|
|
6830
|
-
createdAt: data.created_at
|
|
6831
|
-
};
|
|
6832
|
-
}
|
|
6833
|
-
/**
|
|
6834
|
-
* 로비 참가
|
|
6835
|
-
*/
|
|
6836
|
-
async joinLobby(lobbyId, playerId, displayName, password) {
|
|
6837
|
-
const id = this.appId || "";
|
|
6838
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/join`, {
|
|
6839
|
-
method: "POST",
|
|
6840
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6841
|
-
body: JSON.stringify({ player_id: playerId, display_name: displayName, password })
|
|
6842
|
-
});
|
|
6843
|
-
if (!response.ok) {
|
|
6844
|
-
const err = await response.json().catch(() => ({}));
|
|
6845
|
-
throw new Error(err.error || `Failed to join lobby: ${response.statusText}`);
|
|
6846
|
-
}
|
|
6847
|
-
const data = await response.json();
|
|
6848
|
-
return { lobbyId: data.lobby_id };
|
|
6849
|
-
}
|
|
6850
|
-
/**
|
|
6851
|
-
* 로비 퇴장
|
|
6852
|
-
*/
|
|
6853
|
-
async leaveLobby(lobbyId, playerId) {
|
|
6854
|
-
const id = this.appId || "";
|
|
6855
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/leave`, {
|
|
6856
|
-
method: "POST",
|
|
6857
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6858
|
-
body: JSON.stringify({ player_id: playerId })
|
|
6859
|
-
});
|
|
6860
|
-
if (!response.ok) {
|
|
6861
|
-
const err = await response.json().catch(() => ({}));
|
|
6862
|
-
throw new Error(err.error || `Failed to leave lobby: ${response.statusText}`);
|
|
6863
|
-
}
|
|
6864
|
-
}
|
|
6865
|
-
/**
|
|
6866
|
-
* 레디 상태 토글
|
|
6867
|
-
*/
|
|
6868
|
-
async toggleReady(lobbyId, playerId, ready) {
|
|
6869
|
-
const id = this.appId || "";
|
|
6870
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/ready`, {
|
|
6871
|
-
method: "POST",
|
|
6872
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6873
|
-
body: JSON.stringify({ player_id: playerId, ready })
|
|
6874
|
-
});
|
|
6875
|
-
if (!response.ok) {
|
|
6876
|
-
const err = await response.json().catch(() => ({}));
|
|
6877
|
-
throw new Error(err.error || `Failed to toggle ready: ${response.statusText}`);
|
|
6878
|
-
}
|
|
6879
|
-
}
|
|
6880
|
-
/**
|
|
6881
|
-
* 게임 시작 (호스트만)
|
|
6882
|
-
*/
|
|
6883
|
-
async startGame(lobbyId, hostId) {
|
|
6884
|
-
const id = this.appId || "";
|
|
6885
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/start`, {
|
|
6886
|
-
method: "POST",
|
|
6887
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6888
|
-
body: JSON.stringify({ host_id: hostId })
|
|
6889
|
-
});
|
|
6890
|
-
if (!response.ok) {
|
|
6891
|
-
const err = await response.json().catch(() => ({}));
|
|
6892
|
-
throw new Error(err.error || `Failed to start game: ${response.statusText}`);
|
|
6893
|
-
}
|
|
6894
|
-
const data = await response.json();
|
|
6895
|
-
return { roomId: data.room_id };
|
|
6896
|
-
}
|
|
6897
|
-
/**
|
|
6898
|
-
* 플레이어 추방 (호스트만)
|
|
6899
|
-
*/
|
|
6900
|
-
async kickPlayer(lobbyId, hostId, targetPlayerId) {
|
|
6901
|
-
const id = this.appId || "";
|
|
6902
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/kick/${targetPlayerId}`, {
|
|
6903
|
-
method: "POST",
|
|
6904
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6905
|
-
body: JSON.stringify({ host_id: hostId })
|
|
6906
|
-
});
|
|
6907
|
-
if (!response.ok) {
|
|
6908
|
-
const err = await response.json().catch(() => ({}));
|
|
6909
|
-
throw new Error(err.error || `Failed to kick player: ${response.statusText}`);
|
|
6910
|
-
}
|
|
6911
|
-
}
|
|
6912
|
-
/**
|
|
6913
|
-
* 로비 설정 업데이트 (호스트만)
|
|
6914
|
-
*/
|
|
6915
|
-
async updateLobby(lobbyId, req) {
|
|
6916
|
-
const id = this.appId || "";
|
|
6917
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}`, {
|
|
6918
|
-
method: "PATCH",
|
|
6919
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6920
|
-
body: JSON.stringify({
|
|
6921
|
-
host_id: req.hostId,
|
|
6922
|
-
name: req.name,
|
|
6923
|
-
max_players: req.maxPlayers,
|
|
6924
|
-
visibility: req.visibility,
|
|
6925
|
-
password: req.password,
|
|
6926
|
-
settings: req.settings,
|
|
6927
|
-
tags: req.tags
|
|
6928
|
-
})
|
|
6929
|
-
});
|
|
6930
|
-
if (!response.ok) {
|
|
6931
|
-
const err = await response.json().catch(() => ({}));
|
|
6932
|
-
throw new Error(err.error || `Failed to update lobby: ${response.statusText}`);
|
|
6933
|
-
}
|
|
6934
|
-
const data = await response.json();
|
|
6935
|
-
return {
|
|
6936
|
-
id: data.id,
|
|
6937
|
-
name: data.name,
|
|
6938
|
-
hostId: "",
|
|
6939
|
-
gameType: "",
|
|
6940
|
-
playerCount: 0,
|
|
6941
|
-
maxPlayers: data.max_players,
|
|
6942
|
-
hasPassword: false,
|
|
6943
|
-
visibility: data.visibility,
|
|
6944
|
-
region: "",
|
|
6945
|
-
settings: data.settings,
|
|
6946
|
-
tags: data.tags,
|
|
6947
|
-
status: "",
|
|
6948
|
-
createdAt: ""
|
|
6949
|
-
};
|
|
6950
|
-
}
|
|
6951
|
-
/**
|
|
6952
|
-
* 로비 채팅 전송
|
|
6953
|
-
*/
|
|
6954
|
-
async sendLobbyChat(lobbyId, playerId, message) {
|
|
6955
|
-
const id = this.appId || "";
|
|
6956
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/chat`, {
|
|
6957
|
-
method: "POST",
|
|
6958
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6959
|
-
body: JSON.stringify({ player_id: playerId, message })
|
|
6960
|
-
});
|
|
6961
|
-
if (!response.ok) {
|
|
6962
|
-
const err = await response.json().catch(() => ({}));
|
|
6963
|
-
throw new Error(err.error || `Failed to send chat: ${response.statusText}`);
|
|
6964
|
-
}
|
|
6965
|
-
}
|
|
6966
|
-
/**
|
|
6967
|
-
* 플레이어 초대
|
|
6968
|
-
*/
|
|
6969
|
-
async invitePlayer(lobbyId, inviterId, inviteeId) {
|
|
6970
|
-
const id = this.appId || "";
|
|
6971
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/${lobbyId}/invite`, {
|
|
6972
|
-
method: "POST",
|
|
6973
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
6974
|
-
body: JSON.stringify({ inviter_id: inviterId, invitee_id: inviteeId })
|
|
6975
|
-
});
|
|
6976
|
-
if (!response.ok) {
|
|
6977
|
-
const err = await response.json().catch(() => ({}));
|
|
6978
|
-
throw new Error(err.error || `Failed to invite player: ${response.statusText}`);
|
|
6979
|
-
}
|
|
6980
|
-
const data = await response.json();
|
|
6981
|
-
return {
|
|
6982
|
-
inviteId: data.invite_id,
|
|
6983
|
-
lobbyId: data.lobby_id,
|
|
6984
|
-
lobbyName: data.lobby_name,
|
|
6985
|
-
inviterId: data.inviter_id,
|
|
6986
|
-
inviteeId: data.invitee_id,
|
|
6987
|
-
status: "pending",
|
|
6988
|
-
createdAt: "",
|
|
6989
|
-
expiresAt: data.expires_at
|
|
6990
|
-
};
|
|
6991
|
-
}
|
|
6992
|
-
/**
|
|
6993
|
-
* 초대 수락
|
|
6994
|
-
*/
|
|
6995
|
-
async acceptInvite(inviteId, playerId, displayName) {
|
|
6996
|
-
const id = this.appId || "";
|
|
6997
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/invites/${inviteId}/accept`, {
|
|
6998
|
-
method: "POST",
|
|
6999
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
7000
|
-
body: JSON.stringify({ player_id: playerId, display_name: displayName })
|
|
7001
|
-
});
|
|
7002
|
-
if (!response.ok) {
|
|
7003
|
-
const err = await response.json().catch(() => ({}));
|
|
7004
|
-
throw new Error(err.error || `Failed to accept invite: ${response.statusText}`);
|
|
7005
|
-
}
|
|
7006
|
-
const data = await response.json();
|
|
7007
|
-
return { lobbyId: data.lobby_id };
|
|
7008
|
-
}
|
|
7009
|
-
/**
|
|
7010
|
-
* 초대 거절
|
|
7011
|
-
*/
|
|
7012
|
-
async declineInvite(inviteId, playerId) {
|
|
7013
|
-
const id = this.appId || "";
|
|
7014
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/invites/${inviteId}/decline`, {
|
|
7015
|
-
method: "POST",
|
|
7016
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
7017
|
-
body: JSON.stringify({ player_id: playerId })
|
|
7018
|
-
});
|
|
7019
|
-
if (!response.ok) {
|
|
7020
|
-
const err = await response.json().catch(() => ({}));
|
|
7021
|
-
throw new Error(err.error || `Failed to decline invite: ${response.statusText}`);
|
|
7022
|
-
}
|
|
7023
|
-
}
|
|
7024
|
-
/**
|
|
7025
|
-
* 플레이어의 받은 초대 목록
|
|
7026
|
-
*/
|
|
7027
|
-
async getPlayerInvites(playerId) {
|
|
7028
|
-
const id = this.appId || "";
|
|
7029
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/lobbies/player/${playerId}/invites`, {
|
|
7030
|
-
headers: this.getHeaders()
|
|
7031
|
-
});
|
|
7032
|
-
if (!response.ok) {
|
|
7033
|
-
throw new Error(`Failed to get invites: ${response.statusText}`);
|
|
7034
|
-
}
|
|
7035
|
-
const data = await response.json();
|
|
7036
|
-
return (data.invites || []).map((inv) => ({
|
|
7037
|
-
inviteId: inv.invite_id,
|
|
7038
|
-
lobbyId: inv.lobby_id,
|
|
7039
|
-
lobbyName: inv.lobby_name,
|
|
7040
|
-
inviterId: inv.inviter_id,
|
|
7041
|
-
inviteeId: "",
|
|
7042
|
-
status: inv.status,
|
|
7043
|
-
createdAt: inv.created_at,
|
|
7044
|
-
expiresAt: inv.expires_at
|
|
7045
|
-
}));
|
|
7046
|
-
}
|
|
7047
|
-
// --- Party API ---
|
|
7048
|
-
// 1.1.0 부터 `createParty/leaveParty/kickFromParty/inviteToParty/sendPartyChat` 활성화.
|
|
7049
|
-
// 1.3.0 부터 `acceptPartyInvite/declinePartyInvite` 추가. 직접 join 엔드포인트는 없음 — `joinParty` 는 throw.
|
|
7050
|
-
async createParty(playerId, maxSize) {
|
|
7051
|
-
const id = this.appId || "";
|
|
7052
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties`, {
|
|
7053
|
-
method: "POST",
|
|
7054
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
7055
|
-
body: JSON.stringify({ player_id: playerId, max_size: maxSize || 4 })
|
|
7056
|
-
});
|
|
7057
|
-
if (!response.ok) throw new Error(`Failed to create party: ${response.statusText}`);
|
|
7058
|
-
return response.json();
|
|
7059
|
-
}
|
|
7060
|
-
/**
|
|
7061
|
-
* @deprecated 백엔드에서 직접 join 엔드포인트 미제공 — `inviteToParty` → `acceptPartyInvite` 플로우 사용
|
|
7062
|
-
*/
|
|
7063
|
-
async joinParty(_partyId, _playerId) {
|
|
7064
|
-
throw new Error("cb.game.joinParty is not supported \u2014 use inviteToParty + acceptPartyInvite flow.");
|
|
7065
|
-
}
|
|
7066
|
-
/**
|
|
7067
|
-
* 파티 초대 수락
|
|
7068
|
-
*
|
|
7069
|
-
* `inviteToParty` 로 생성된 초대 ID 를 수락하여 파티에 합류한다.
|
|
7070
|
-
* 로비 초대(`acceptInvite`) 와 다른 엔드포인트 (`/v1/game/:appID/invites/:inviteID/accept`).
|
|
7071
|
-
*/
|
|
7072
|
-
async acceptPartyInvite(inviteId, playerId, displayName) {
|
|
7073
|
-
const id = this.appId || "";
|
|
7074
|
-
const qs = new URLSearchParams({ player_id: playerId });
|
|
7075
|
-
if (displayName) qs.set("display_name", displayName);
|
|
7076
|
-
const response = await fetch(
|
|
7077
|
-
`${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/accept?${qs.toString()}`,
|
|
7078
|
-
{ method: "POST", headers: this.getHeaders() }
|
|
7079
|
-
);
|
|
7080
|
-
if (!response.ok) {
|
|
7081
|
-
const err = await response.json().catch(() => ({}));
|
|
7082
|
-
throw new Error(err.error || `Failed to accept party invite: ${response.statusText}`);
|
|
7083
|
-
}
|
|
7084
|
-
return response.json();
|
|
7085
|
-
}
|
|
7086
|
-
/**
|
|
7087
|
-
* 파티 초대 거절
|
|
7088
|
-
*
|
|
7089
|
-
* `inviteToParty` 로 생성된 초대 ID 를 거절한다.
|
|
7090
|
-
*/
|
|
7091
|
-
async declinePartyInvite(inviteId, playerId) {
|
|
7092
|
-
const id = this.appId || "";
|
|
7093
|
-
const qs = new URLSearchParams({ player_id: playerId });
|
|
7094
|
-
const response = await fetch(
|
|
7095
|
-
`${this.gameServerUrl}/v1/game/${id}/invites/${inviteId}/decline?${qs.toString()}`,
|
|
7096
|
-
{ method: "POST", headers: this.getHeaders() }
|
|
7097
|
-
);
|
|
7098
|
-
if (!response.ok) {
|
|
7099
|
-
const err = await response.json().catch(() => ({}));
|
|
7100
|
-
throw new Error(err.error || `Failed to decline party invite: ${response.statusText}`);
|
|
7101
|
-
}
|
|
7102
|
-
}
|
|
7103
|
-
async leaveParty(partyId, playerId) {
|
|
7104
|
-
const id = this.appId || "";
|
|
7105
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/leave`, {
|
|
7106
|
-
method: "POST",
|
|
7107
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
7108
|
-
body: JSON.stringify({ player_id: playerId })
|
|
7109
|
-
});
|
|
7110
|
-
if (!response.ok) throw new Error(`Failed to leave party: ${response.statusText}`);
|
|
7111
|
-
}
|
|
7112
|
-
async kickFromParty(partyId, playerId) {
|
|
7113
|
-
const id = this.appId || "";
|
|
7114
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/kick/${playerId}`, {
|
|
7115
|
-
method: "POST",
|
|
7116
|
-
headers: this.getHeaders()
|
|
7117
|
-
});
|
|
7118
|
-
if (!response.ok) throw new Error(`Failed to kick from party: ${response.statusText}`);
|
|
7119
|
-
}
|
|
7120
|
-
async inviteToParty(partyId, inviterId, inviteeId) {
|
|
7121
|
-
const id = this.appId || "";
|
|
7122
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/invite/${inviteeId}`, {
|
|
7123
|
-
method: "POST",
|
|
7124
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
7125
|
-
body: JSON.stringify({ inviter_id: inviterId })
|
|
7126
|
-
});
|
|
7127
|
-
if (!response.ok) throw new Error(`Failed to invite to party: ${response.statusText}`);
|
|
7128
|
-
return response.json();
|
|
7129
|
-
}
|
|
7130
|
-
async sendPartyChat(partyId, playerId, message) {
|
|
7131
|
-
const id = this.appId || "";
|
|
7132
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/parties/${partyId}/chat`, {
|
|
7133
|
-
method: "POST",
|
|
7134
|
-
headers: { ...this.getHeaders(), "Content-Type": "application/json" },
|
|
7135
|
-
body: JSON.stringify({ sender_id: playerId, content: message })
|
|
7136
|
-
});
|
|
7137
|
-
if (!response.ok) throw new Error(`Failed to send party chat: ${response.statusText}`);
|
|
7138
|
-
}
|
|
7139
6615
|
// --- Spectator API ---
|
|
7140
6616
|
// 1.1.0 부터 활성화. 백엔드 엔드포인트는 `/rooms/:roomID/spectators` 를 사용 (기존 `/spectate` 아님).
|
|
7141
6617
|
async joinSpectator(roomId, playerId) {
|
|
@@ -7165,36 +6641,6 @@ var GameAPI = class {
|
|
|
7165
6641
|
const data = await response.json();
|
|
7166
6642
|
return data.spectators || [];
|
|
7167
6643
|
}
|
|
7168
|
-
// --- Ranking/Leaderboard API ---
|
|
7169
|
-
// 1.1.0 부터 활성화. 백엔드가 `app_id` + `game_type` 쿼리를 요구하므로 SDK 시그니처에 `gameType` 추가.
|
|
7170
|
-
async getLeaderboard(gameType, top = 100, season = "default") {
|
|
7171
|
-
const id = this.appId || "";
|
|
7172
|
-
const params = new URLSearchParams({ app_id: id, game_type: gameType, season, limit: String(top) });
|
|
7173
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/leaderboard/top?${params}`, {
|
|
7174
|
-
headers: this.getHeaders()
|
|
7175
|
-
});
|
|
7176
|
-
if (!response.ok) throw new Error(`Failed to get leaderboard: ${response.statusText}`);
|
|
7177
|
-
const data = await response.json();
|
|
7178
|
-
return data.entries || [];
|
|
7179
|
-
}
|
|
7180
|
-
async getPlayerStats(playerId, gameType, season = "default") {
|
|
7181
|
-
const id = this.appId || "";
|
|
7182
|
-
const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
|
|
7183
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/stats?${params}`, {
|
|
7184
|
-
headers: this.getHeaders()
|
|
7185
|
-
});
|
|
7186
|
-
if (!response.ok) throw new Error(`Failed to get player stats: ${response.statusText}`);
|
|
7187
|
-
return response.json();
|
|
7188
|
-
}
|
|
7189
|
-
async getPlayerRank(playerId, gameType, season = "default") {
|
|
7190
|
-
const id = this.appId || "";
|
|
7191
|
-
const params = new URLSearchParams({ app_id: id, game_type: gameType, season });
|
|
7192
|
-
const response = await fetch(`${this.gameServerUrl}/v1/game/${id}/ranking/players/${playerId}/rank?${params}`, {
|
|
7193
|
-
headers: this.getHeaders()
|
|
7194
|
-
});
|
|
7195
|
-
if (!response.ok) throw new Error(`Failed to get player rank: ${response.statusText}`);
|
|
7196
|
-
return response.json();
|
|
7197
|
-
}
|
|
7198
6644
|
// --- Voice API ---
|
|
7199
6645
|
async joinVoiceChannel(roomId, playerId) {
|
|
7200
6646
|
const id = this.appId || "";
|
|
@@ -7285,6 +6731,166 @@ var GameAPI = class {
|
|
|
7285
6731
|
}
|
|
7286
6732
|
return headers;
|
|
7287
6733
|
}
|
|
6734
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
6735
|
+
// Phase 3 — Matchqueue primitive (mechanism only)
|
|
6736
|
+
//
|
|
6737
|
+
// 매칭 알고리즘 (어떤 ticket 끼리 묶을지) 은 사용자 Lua 가 list → notify 흐름으로
|
|
6738
|
+
// 직접 구현. attributes 는 free-form JSON 으로 rating/region/team_size 등 자유롭게 첨부.
|
|
6739
|
+
// 매칭 완료 시 사용자가 cb.realtime.subscribe(`matchqueue.${appID}.${queueKey}`) 로 수신.
|
|
6740
|
+
//
|
|
6741
|
+
// 자세한 패턴: docs/game-server/RECIPES.md §10
|
|
6742
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
6743
|
+
/**
|
|
6744
|
+
* matchqueue 에 ticket 등록.
|
|
6745
|
+
*
|
|
6746
|
+
* @example
|
|
6747
|
+
* await cb.game.enqueueMatch(appId, "ranked", userId, {
|
|
6748
|
+
* rating: 1500, region: "asia", team_size: 5,
|
|
6749
|
+
* }, 60)
|
|
6750
|
+
*
|
|
6751
|
+
* @constraints appId/queueKey/ticketId 는 [a-zA-Z0-9_-] 만 허용. ttlSec=0 이면 만료 없음.
|
|
6752
|
+
*/
|
|
6753
|
+
async enqueueMatch(appId, queueKey, ticketId, attributes, ttlSec) {
|
|
6754
|
+
return this.http.post(
|
|
6755
|
+
`/v1/game/${appId}/matchqueue/${queueKey}/tickets`,
|
|
6756
|
+
{ ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 }
|
|
6757
|
+
);
|
|
6758
|
+
}
|
|
6759
|
+
/**
|
|
6760
|
+
* matchqueue 의 모든 ticket 목록 반환. 사용자 Lua 가 매칭 후보 선별에 사용.
|
|
6761
|
+
*
|
|
6762
|
+
* @example const tickets = await cb.game.listMatchqueue(appId, "ranked")
|
|
6763
|
+
*/
|
|
6764
|
+
async listMatchqueue(appId, queueKey) {
|
|
6765
|
+
return this.http.get(
|
|
6766
|
+
`/v1/game/${appId}/matchqueue/${queueKey}`
|
|
6767
|
+
);
|
|
6768
|
+
}
|
|
6769
|
+
/**
|
|
6770
|
+
* 매칭 큐에서 ticket 제거 (예: 사용자가 매칭 취소).
|
|
6771
|
+
*/
|
|
6772
|
+
async cancelMatch(appId, queueKey, ticketId) {
|
|
6773
|
+
await this.http.delete(
|
|
6774
|
+
`/v1/game/${appId}/matchqueue/${queueKey}/tickets/${ticketId}`
|
|
6775
|
+
);
|
|
6776
|
+
}
|
|
6777
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
6778
|
+
// Phase 4 — Leaderboard primitive (mechanism only)
|
|
6779
|
+
//
|
|
6780
|
+
// 시즌은 사용자가 leaderboardKey suffix 로 분리 (예: "ranks:2026q2"). ELO/티어 공식은
|
|
6781
|
+
// 사용자 Lua 가 incrScore 호출로 결과 기록 (RECIPES.md §6 참고).
|
|
6782
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
6783
|
+
/**
|
|
6784
|
+
* leaderboard 점수 기록. mode="set" (기본, overwrite) 또는 "incr" (증감).
|
|
6785
|
+
*
|
|
6786
|
+
* @example
|
|
6787
|
+
* await cb.game.submitScore(appId, "global_elo", userId, 32, "incr")
|
|
6788
|
+
*/
|
|
6789
|
+
async submitScore(appId, leaderboardKey, member, score, mode = "set") {
|
|
6790
|
+
return this.http.post(
|
|
6791
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/scores`,
|
|
6792
|
+
{ member, score, mode }
|
|
6793
|
+
);
|
|
6794
|
+
}
|
|
6795
|
+
/**
|
|
6796
|
+
* 상위 n 명 조회 (기본 10).
|
|
6797
|
+
*/
|
|
6798
|
+
async getTopScores(appId, leaderboardKey, n = 10) {
|
|
6799
|
+
return this.http.get(
|
|
6800
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/top?n=${n}`
|
|
6801
|
+
);
|
|
6802
|
+
}
|
|
6803
|
+
/**
|
|
6804
|
+
* 단일 member 의 rank + score.
|
|
6805
|
+
*/
|
|
6806
|
+
async getMemberRank(appId, leaderboardKey, member) {
|
|
6807
|
+
return this.http.get(
|
|
6808
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`
|
|
6809
|
+
);
|
|
6810
|
+
}
|
|
6811
|
+
/**
|
|
6812
|
+
* member 주변 (위 above 명 + 본인 + 아래 below 명) 조회.
|
|
6813
|
+
*/
|
|
6814
|
+
async getRankAround(appId, leaderboardKey, member, above = 5, below = 5) {
|
|
6815
|
+
return this.http.get(
|
|
6816
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/around/${member}?above=${above}&below=${below}`
|
|
6817
|
+
);
|
|
6818
|
+
}
|
|
6819
|
+
/**
|
|
6820
|
+
* leaderboard 전체 reset (시즌 종료 시).
|
|
6821
|
+
*/
|
|
6822
|
+
async resetLeaderboard(appId, leaderboardKey) {
|
|
6823
|
+
await this.http.delete(`/v1/game/${appId}/leaderboards/${leaderboardKey}`);
|
|
6824
|
+
}
|
|
6825
|
+
/**
|
|
6826
|
+
* 특정 member 만 제거 (계정 삭제 등).
|
|
6827
|
+
*/
|
|
6828
|
+
async removeFromLeaderboard(appId, leaderboardKey, member) {
|
|
6829
|
+
await this.http.delete(
|
|
6830
|
+
`/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`
|
|
6831
|
+
);
|
|
6832
|
+
}
|
|
6833
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
6834
|
+
// Phase 5 — Lua 스크립트 관리 (메타데이터 + 버전 + hot reload)
|
|
6835
|
+
//
|
|
6836
|
+
// 활성화 시 NATS publish 로 모든 게임 서버 인스턴스가 VM pool 핫 리로드.
|
|
6837
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
6838
|
+
/**
|
|
6839
|
+
* 새 스크립트 버전 업로드 (latest+1). idempotent — 동일 hash 면 새 버전 만들지 않음.
|
|
6840
|
+
*
|
|
6841
|
+
* @example
|
|
6842
|
+
* await cb.game.uploadScript(appId, "ranked_br", fs.readFileSync("./ranked.lua", "utf-8"))
|
|
6843
|
+
*/
|
|
6844
|
+
async uploadScript(appId, name, code) {
|
|
6845
|
+
return this.http.post(
|
|
6846
|
+
`/v1/game/${appId}/scripts`,
|
|
6847
|
+
{ name, code }
|
|
6848
|
+
);
|
|
6849
|
+
}
|
|
6850
|
+
/**
|
|
6851
|
+
* app 의 모든 스크립트 메타데이터 목록.
|
|
6852
|
+
*/
|
|
6853
|
+
async listScripts(appId) {
|
|
6854
|
+
return this.http.get(`/v1/game/${appId}/scripts`);
|
|
6855
|
+
}
|
|
6856
|
+
/**
|
|
6857
|
+
* 단일 스크립트 메타 + active version code.
|
|
6858
|
+
*/
|
|
6859
|
+
async getScript(appId, name) {
|
|
6860
|
+
return this.http.get(`/v1/game/${appId}/scripts/${name}`);
|
|
6861
|
+
}
|
|
6862
|
+
/**
|
|
6863
|
+
* 단일 스크립트의 모든 버전 이력.
|
|
6864
|
+
*/
|
|
6865
|
+
async listScriptVersions(appId, name) {
|
|
6866
|
+
return this.http.get(
|
|
6867
|
+
`/v1/game/${appId}/scripts/${name}/versions`
|
|
6868
|
+
);
|
|
6869
|
+
}
|
|
6870
|
+
/**
|
|
6871
|
+
* 특정 버전 활성화 (hot reload 자동). version=0 또는 미지정 → latest 활성화.
|
|
6872
|
+
*/
|
|
6873
|
+
async activateScript(appId, name, version) {
|
|
6874
|
+
return this.http.post(
|
|
6875
|
+
`/v1/game/${appId}/scripts/${name}/activate`,
|
|
6876
|
+
{ version: version ?? 0 }
|
|
6877
|
+
);
|
|
6878
|
+
}
|
|
6879
|
+
/**
|
|
6880
|
+
* 직전 active 버전으로 롤백 (hot reload 자동).
|
|
6881
|
+
*/
|
|
6882
|
+
async rollbackScript(appId, name) {
|
|
6883
|
+
return this.http.post(
|
|
6884
|
+
`/v1/game/${appId}/scripts/${name}/rollback`,
|
|
6885
|
+
{}
|
|
6886
|
+
);
|
|
6887
|
+
}
|
|
6888
|
+
/**
|
|
6889
|
+
* 스크립트 비활성화 (status=disabled). 코드/버전은 보존.
|
|
6890
|
+
*/
|
|
6891
|
+
async disableScript(appId, name) {
|
|
6892
|
+
await this.http.delete(`/v1/game/${appId}/scripts/${name}`);
|
|
6893
|
+
}
|
|
7288
6894
|
};
|
|
7289
6895
|
|
|
7290
6896
|
// src/api/ads.ts
|