SDK là một file JavaScript không phụ thuộc thư viện nào (khoảng 8 KB gzip), tạo biến toàn cục window.WSG. Mọi hàm bất đồng bộ trả Promise; ngoài portal, mọi hàm vẫn trả kết quả hợp lệ chứ không ném lỗi vì môi trường. Trang này mô tả bản 1.x, phục vụ ở /sdk/v1/wsg-sdk.js.
Danh sách API
| API | Trả về | Gọi trước init() |
|---|---|---|
WSG.version, WSG.environment | string, Environment | Được |
WSG.init(options) | Promise<InitContext> | — |
WSG.isAvailable(feature) | boolean | Trả false |
WSG.player, getToken(), login() | Player, Promise<string | null>, Promise<Player> | Hàm bị từ chối |
loadingStart(), loadingProgress(), loadingFinished() | void | Bị bỏ qua |
gameplayStart(), gameplayStop() | void | Bị bỏ qua |
happyTime(intensity?) | void | Bị bỏ qua |
track(name, props?) | void | Bị bỏ qua |
ads.interstitial(), ads.rewarded(), ads.isBlocked() | Promise<AdResult>, boolean | Bị từ chối (isBlocked() trả false) |
leaderboard.submit(), top(), around(), me(), show() | Promise<…> | Bị từ chối |
storage.get(), set(), remove(), getAll() | Promise<…> | Bị từ chối |
share(payload) | Promise<{ ok: boolean }> | Bị từ chối |
getLocale(), getEntryParams() | string, Record<string, string> | Được |
on(event, cb), off(event, cb) | hàm huỷ, void | Được |
"Bị từ chối" nghĩa là Promise reject với mã not_initialized. "Bị bỏ qua" nghĩa là lời gọi không có tác dụng (bật debug sẽ thấy cảnh báo trong console). Lời gọi trong lúc init() đang chạy cũng tính là trước init(), nên luôn await WSG.init() trước.
Nhúng SDK và kiểu TypeScript
Game ngoài repo nhúng SDK bằng thẻ script với đường dẫn gốc trên origin game:
<script src="/sdk/v1/wsg-sdk.js"></script>Game trong repo WebSuperGames có thể import bản ESM của package nội bộ @wsg/sdk (import { WSG } from '@wsg/sdk'), nhưng template Phaser vẫn ưu tiên thẻ script để luôn dùng bản SDK mới nhất mà portal phục vụ. Package này không phát hành công khai; nếu bạn dùng TypeScript với thẻ script, chép file khai báo ở mục Kiểu TypeScript.
Thuộc tính
WSG.version
readonly version: string;Phiên bản đầy đủ theo semver của SDK đang chạy, ví dụ "1.0.0". Major version nằm trong đường dẫn /sdk/v1/; trong cùng major, SDK tương thích ngược.
WSG.environment
readonly environment: 'portal' | 'standalone' | 'disabled';Môi trường SDK đang chạy. Trước khi init() xong, giá trị luôn là 'standalone'.
| Giá trị | Khi nào | Hành vi |
|---|---|---|
portal | Game nằm trong iframe, URL có wsg=1 và host=<origin http(s)>, và portal trả lời bắt tay trong 3 giây | Mọi yêu cầu đi qua postMessage tới trang portal; SDK chỉ nhận message có source là window.parent và origin đúng bằng host |
standalone | Mở game trực tiếp, thiếu query, hoặc portal không trả lời trong 3 giây | Chạy cục bộ: người chơi giả, bảng xếp hạng và storage trong localStorage, quảng cáo giả 1 giây |
disabled | Portal trả lời bắt tay bằng { disabled: true } (ví dụ phiên chơi đã kết thúc) | Mọi API trả giá trị mặc định ngay, không gửi gì đi; vẫn nhận sự kiện pause/resume từ portal |
Khởi tạo
WSG.init(options)
init(options: InitOptions): Promise<InitContext>;
interface InitOptions {
gameId: string; // slug trong game.json
version?: string; // phiên bản game, ví dụ "1.0.0"
debug?: boolean; // in log [WSG] ra console
}
interface InitContext {
environment: 'portal' | 'standalone' | 'disabled';
player: Player;
locale: string; // 'vi', 'en', ...
features: Record<Feature, boolean>;
entryParams: Record<string, string>;
}gameIdbắt buộc: chuỗi không rỗng, tối đa 64 ký tự, không chứa khoảng trắng; sai thì reject vớiinvalid_argument. Dùng đúngslugcủa game. Portal sogameIdvới game đang mở và ghi cảnh báo nếu khác, nhưng vẫn cho chạy.versionđược gửi kèm lời bắt tay; hiện portal không dùng giá trị này.debug: truein ra console các cảnh báo hữu ích khi phát triển: lời gọi bị bỏ qua vì chưainit, rơi về standalone vì portal không trả lời,tracksai tên, nộp điểm ngoài khung gameplay…- Gọi
init()lần nữa trả về cùng Promise và cùng kết quả; tham số của các lần gọi sau bị bỏ qua. Nếu lần trước bị reject vì tham số sai, bạn gọi lại được. init()không reject vì môi trường. Trong iframe portal mà không nhận được trả lời sau 3 giây, SDK chuyển sangstandalone(bậtdebugsẽ thấy cảnh báo).ctx.playerlà bản sao người chơi lúc khởi tạo; muốn đọc giá trị mới nhất, dùngWSG.player.ctx.locale: trong portal là ngôn ngữ giao diện portal (vihoặcen); ở standalone lấy từ querylang, rồinavigator.language, cuối cùng làen. Luôn là mã 2–3 chữ cái thường.ctx.features: xemisAvailable().ctx.entryParams: xemgetEntryParams().
const ctx = await WSG.init({ gameId: 'tap-rush', version: '1.0.0', debug: true });
if (ctx.environment === 'portal') {
console.log('Đang chạy trên portal, người chơi', ctx.player.name, 'ngôn ngữ', ctx.locale);
}WSG.isAvailable(feature)
isAvailable(feature: Feature): boolean;
type Feature = 'ads.interstitial' | 'ads.rewarded' | 'leaderboard' | 'storage' | 'share' | 'auth';Cho biết tính năng có bật cho game trong môi trường hiện tại không. Trước init() luôn trả false.
- Portal: một tính năng bật khi game khai báo dùng trong khối
featurescủagame.jsonở phiên bản đang chạy và đội vận hành đang bật nó cho game trong trang admin.sharekhông có tronggame.jsonnên chỉ theo công tắc admin;authluônfalsevì portal chưa có đăng nhập. - Standalone: mọi tính năng
truetrừauth. - Disabled: mọi tính năng
false.
Khi một tính năng tắt, portal chặn lời gọi tương ứng: ads.interstitial() và ads.rewarded() trả { shown: false, reason: 'disabled' } (rewarded có thêm rewarded: false), các hàm leaderboard.* và storage.* bị từ chối với lỗi unavailable, share() trả { ok: false }. Server cũng tự chặn: nộp điểm khi bảng xếp hạng tắt nhận reason: 'disabled'. Vì đội vận hành có thể đổi công tắc bất cứ lúc nào, hãy đọc cờ sau init() để quyết định giao diện, ví dụ ẩn nút "Xem quảng cáo để hồi sinh".
if (WSG.isAvailable('ads.rewarded')) showReviveButton();Người chơi
WSG.player
interface Player {
id: string;
name: string;
avatarUrl: string | null;
isGuest: boolean;
}
player: Player & {
getToken(): Promise<string | null>;
login(): Promise<Player>;
};- Trước
init():{ id: '', name: 'Guest', avatarUrl: null, isGuest: true }. - Portal:
idlà mã người chơi (UUID) trên portal,namelà tên hiển thị (người chơi tự đổi được ở portal),isGuestlàtruevới tài khoản khách. Bản v1 chỉ có tài khoản khách. - Standalone:
iddạnglocal-<12 ký tự>, giữ tronglocalStorage(khoáwsg:local-id),namelàGuest. - Các trường được cập nhật tại chỗ khi có sự kiện
authchange, nên đọcWSG.player.namelúc cần thay vì lưu lại một lần.
WSG.player.getToken()
getToken(): Promise<string | null>;- Portal: trả JWT RS256 của phiên chơi hiện tại, dùng để server riêng của game xác thực người chơi. Cả phiên dùng chung một token; token hết hạn sau 1 giờ kể từ lúc phiên bắt đầu và portal chưa cấp lại trong cùng phiên. Claims và cách kiểm chứng ở trang Điểm và bảng xếp hạng.
- Standalone và disabled:
null.
WSG.player.login()
login(): Promise<Player>;Portal v1 chưa có hộp thoại đăng nhập, nên login() trả về người chơi hiện tại. Nếu thông tin người chơi khác với WSG.player, SDK cập nhật WSG.player và phát authchange. Ở standalone trả người chơi cục bộ.
Vòng đời
loadingStart(), loadingProgress(), loadingFinished()
loadingStart(): void;
loadingProgress(percent: number): void; // 0..100
loadingFinished(): void;percentđược làm tròn và kẹp trong 0..100; giá trị không phải số bị bỏ qua.- Portal hiện overlay tải với phần trăm bạn báo và chỉ gỡ khi nhận
loadingFinished(). Overlay che và chặn thao tác lên game. Nếu sau 30 giây vẫn chưa nhận, portal tự gỡ overlay và ghi sự kiệnload_timeout. - Portal ghi các sự kiện
load_start,load_end, vàload_progressmỗi khi phần trăm vượt mốc 25, 50, 75, 100 (không ghi từng lần báo). loadingStart()không bắt buộc.loadingFinished()thì bắt buộc.
gameplayStart(), gameplayStop()
gameplayStart(): void;
gameplayStop(): void;- Gọi
gameplayStart()khi người chơi thật sự bắt đầu điều khiển;gameplayStop()khi game over, về menu hoặc người chơi tạm dừng. - SDK bỏ qua lời gọi trùng trạng thái:
gameplayStart()khi đang chạy,gameplayStop()khi đã dừng. gameplayStart()gọi trước khiinit()xong bị bỏ qua, và điểm nộp sau đó sẽ bị từ chối vớino_gameplay.- Trong portal,
gameplayStart()đầu tiên của phiên được tính là một lượt chơi của game (bảng "chơi nhiều") và cộng XP cho người chơi; thời gian gameplay cũng được quy ra XP. Portal gửi hai sự kiện này lên server ngay, không chờ gom. - Khi bạn gọi quảng cáo lúc gameplay đang chạy, SDK tự gọi
gameplayStop(). Nếu lượt chơi tiếp tục sau quảng cáo, gọi lạigameplayStart(). - Điểm chỉ được nhận khi gameplay đang chạy hoặc trong 10 giây sau
gameplayStop().
Cảnh báo
Server tính thời gian chơi để kiểm tra điểm từ lần gameplayStart() gần nhất. Dừng rồi chạy lại gameplay giữa một lượt (tạm dừng, hồi sinh bằng quảng cáo) làm mốc này bị đặt lại. Xem cách xử lý ở Điểm và bảng xếp hạng.
happyTime(intensity?)
happyTime(intensity?: number): void; // 0..1, mặc định 1Báo một khoảnh khắc vui: phá kỷ lục, qua màn, combo lớn. intensity được kẹp trong 0..1. Portal ghi sự kiện happy và dành tín hiệu này cho hiệu ứng hoặc quảng cáo sau này. Mỗi lần gọi là một sự kiện, tính vào giới hạn sự kiện của phiên (xem track).
Sự kiện tuỳ chỉnh
WSG.track(name, props?)
track(name: string, props?: Record<string, unknown>): void;Ghi một sự kiện phân tích của riêng game, ví dụ level_complete, continue_used.
namekhớp^[a-z][a-z0-9_]{1,63}$: 2–64 ký tự gồm chữ thường, số, gạch dưới, bắt đầu bằng chữ. Tên bắt đầu bằngwsg_dành cho SDK. Tên sai bị bỏ qua.- Không dùng tên trùng sự kiện chuẩn của portal:
load_start,load_progress,load_end,gameplay_start,gameplay_stop,happy,ad_request,ad_result,score_submit,score_rejected,share,session_end,load_timeout. Server xử lý các tên này như sự kiện hệ thống (ví dụ mộttrack('gameplay_start')được coi là gameplay bắt đầu). propslà object JSON, tối đa 2048 byte UTF-8 sau khiJSON.stringify. SDK chuyển qua JSON trước khi gửi: hàm vàundefinedbị bỏ,Datethành chuỗi. Không serialize được hoặc quá cỡ thì sự kiện bị bỏ.- SDK cho tối đa 60 sự kiện trong mỗi cửa sổ 60 giây (tính từ sự kiện đầu của cửa sổ); vượt thì bỏ và đếm, rồi báo trong lần gửi kế bằng sự kiện
wsg_track_dropped { count }. - SDK gom sự kiện, gửi sau 1 giây hoặc khi đủ 20 sự kiện, và gửi ngay khi trang bị đóng (
pagehide). Portal gom tiếp, gửi lên server mỗi 5 giây hoặc khi đủ 20 sự kiện. - Server nhận tối đa 60 sự kiện mỗi phút cho mỗi phiên, tính chung cả sự kiện vòng đời do portal ghi (
load_*,gameplay_*,happy,ad_*,score_submit,share). Sự kiện vượt giới hạn bị bỏ, kể cảgameplay_start— khi đó điểm sẽ bị từ chốino_gameplay. Hãy giữ ở mức vài sự kiện cho mỗi lượt chơi.
WSG.track('level_complete', { level: 3, stars: 2, timeMs: 41250 });Quảng cáo
WSG.ads.interstitial(opts?)
interstitial(opts?: { placement?: string }): Promise<AdResult>;Quảng cáo toàn màn hình giữa các lượt. placement là nhãn tuỳ chọn (tối đa 64 ký tự) để thống kê, ví dụ 'between_rounds', 'menu', 'pause'.
WSG.ads.rewarded(opts?)
rewarded(opts?: { placement?: string }): Promise<AdResult>;Quảng cáo người chơi chủ động xem để nhận thưởng. Kết quả luôn có trường rewarded; chỉ cấp thưởng khi rewarded === true, không dựa vào shown.
WSG.ads.isBlocked()
isBlocked(): boolean;true sau khi có một kết quả quảng cáo với reason: 'adblock' trong phiên; dùng để ẩn các nút gắn với quảng cáo.
Kết quả và reason
interface AdResult {
shown: boolean;
rewarded?: boolean; // chỉ có với rewarded
reason?: 'cooldown' | 'unfilled' | 'adblock' | 'disabled' | 'dismissed' | 'not_available' | 'error';
}Trình tự khi gọi quảng cáo:
- Nếu gameplay đang chạy, SDK gọi
gameplayStop(). - SDK phát
pausevàaudio(false). - Portal hiện quảng cáo (hoặc trả lời ngay với một
reason). - SDK phát
audio(true)vàresume, rồi Promise resolve.
reason | Ý nghĩa | Game nên làm |
|---|---|---|
| (không có) | Quảng cáo đã hiện | Interstitial: chơi tiếp. Rewarded: xem rewarded |
cooldown | Chưa đủ 60 giây kể từ interstitial trước (chỉ interstitial) | Bỏ qua, chơi tiếp |
not_available | Đang có quảng cáo khác | Không gọi chồng; bỏ qua |
dismissed | Người chơi đóng rewarded trước khi xem hết (shown: true, rewarded: false) | Không cấp thưởng |
unfilled | Không có quảng cáo để hiện | Bỏ qua; với rewarded có thể ẩn nút một lúc |
adblock | Trình chặn quảng cáo; isBlocked() thành true | Ẩn nút gắn với quảng cáo |
disabled | SDK ở chế độ disabled | Bỏ qua |
error | Lỗi môi trường, ví dụ portal không trả lời trong 120 giây | Bỏ qua, chơi tiếp |
- Promise chỉ reject khi gọi trước
init()(not_initialized). Mọi sự cố khác trả{ shown: false, reason: … }. - Portal: khoảng cách tối thiểu giữa hai interstitial là 60 giây (tính từ lần hiện gần nhất trong trang); rewarded không có giới hạn này. Hiện portal dùng quảng cáo giả lập: interstitial 3 giây rồi hiện nút Đóng, rewarded 5 giây (đóng sớm bằng Esc thì
dismissed). - Standalone: chờ 1 giây rồi trả
{ shown: true }hoặc{ shown: true, rewarded: true }, không có cooldown. - Hướng dẫn chọn thời điểm và ví dụ hồi sinh ở trang Quảng cáo.
Bảng xếp hạng
type Period = 'daily' | 'weekly' | 'all';
interface LeaderboardEntry {
rank: number;
playerId: string;
name: string;
avatarUrl: string | null;
score: number;
isMe: boolean;
}WSG.leaderboard.submit(score, meta?)
submit(score: number, meta?: Record<string, unknown>): Promise<SubmitResult>;
interface SubmitResult {
accepted: boolean;
best: number; // điểm tốt nhất mọi lúc của người chơi
rank: number | null; // hạng mọi lúc, tính từ 1
improved: boolean; // lần này phá kỷ lục mọi lúc
reason?: string; // chỉ có khi bị từ chối
}scorephải là số hữu hạn, nếu không Promise rejectinvalid_argument. Server còn yêu cầu số nguyên từ 0 tới 2^53; số lẻ hay số âm bị từ chối vớireason: 'invalid_score'. Làm tròn trước khi nộp.metalà object JSON tuỳ chọn, được lưu kèm điểm được nhận (không hiển thị công khai). Giữ gọn.- Bị từ chối không làm Promise reject: kết quả có
accepted: false,reasonlà một tronginvalid_session,invalid_score,rate_limited,no_gameplay,over_max,too_fast,rate_too_high,blocked;bestvàranklà trạng thái hiện có (0 vànullnếu chưa có điểm);improved: false. - Với game
scoreOrder: "asc"(thấp hơn là tốt hơn),bestlà điểm thấp nhất. - Một lần nộp hợp lệ cập nhật cả ba kỳ ngày, tuần, mọi lúc;
ranktrong kết quả là hạng mọi lúc. - Bật
debugsẽ thấy cảnh báo khi nộp ngoài khung gameplay; SDK vẫn gửi và server trảno_gameplay. - Standalone: luôn
accepted: true, không kiểm tra chống gian lận; điểm lưu ởlocalStorage(khoáwsg:<gameId>:lb, mỗi người chơi cục bộ một dòng, giữ 100 dòng cao nhất). - Disabled:
{ accepted: false, best: 0, rank: null, improved: false, reason: 'disabled' }.
Các luật kiểm tra và cách chọn limits: Điểm và bảng xếp hạng.
WSG.gameplayStop();
const result = await WSG.leaderboard.submit(Math.floor(score), { level: 7 });
if (result.accepted && result.improved) showNewRecord(result.best, result.rank);WSG.leaderboard.top(opts?)
top(opts?: { period?: Period; limit?: number }): Promise<LeaderboardEntry[]>;Các dòng đầu bảng. period mặc định 'all' (giá trị khác ba kỳ trên bị reject invalid_argument); limit được làm tròn và kẹp trong 1..100, mặc định 10. Trong portal, kỳ daily là ngày hiện tại và weekly là tuần ISO (bắt đầu thứ Hai) theo giờ Việt Nam (Asia/Ho_Chi_Minh); isMe đánh dấu người chơi hiện tại. Ở standalone chỉ có một bảng cục bộ, period không có tác dụng.
WSG.leaderboard.around(opts?)
around(opts?: { period?: Period; radius?: number }): Promise<LeaderboardEntry[]>;Các dòng quanh vị trí người chơi: từ hạng của họ trừ radius tới cộng radius. radius được kẹp trong 1..10, mặc định 5. Trả mảng rỗng nếu người chơi chưa có điểm trong kỳ đó.
WSG.leaderboard.me(period?)
me(period?: Period): Promise<LeaderboardEntry | null>;Dòng của người chơi hiện tại trong kỳ (mặc định 'all'), null nếu chưa có điểm.
WSG.leaderboard.show(period?)
show(period?: Period): Promise<void>;Yêu cầu portal mở bảng xếp hạng của game (hộp thoại phủ trên trang) ở kỳ được chọn. Promise resolve ngay sau khi portal nhận yêu cầu, không chờ hộp thoại đóng. Portal không phát pause khi mở hộp thoại này, nên hãy gọi từ màn hình menu hoặc game over. Ở standalone không làm gì.
Lưu dữ liệu
get<T = unknown>(key: string): Promise<T | null>;
set(key: string, value: unknown): Promise<void>;
set(entries: Record<string, unknown>): Promise<void>;
remove(key: string): Promise<void>;
getAll(): Promise<Record<string, unknown>>;Kho key-value theo người chơi và theo game. Dùng cho kỷ lục cá nhân, tiến độ, cài đặt.
WSG.storage.get(key)
Giá trị của key, hoặc null nếu chưa có. Đọc được ngay giá trị vừa set mà chưa gửi đi.
WSG.storage.set(key, value) và set(entries)
Ghi một key, hoặc nhiều key cùng lúc với dạng object. Các lần set trong vòng 500 ms được gom thành một lần gửi; Promise resolve khi lần gửi đó xong.
WSG.storage.remove(key)
Xoá một key.
WSG.storage.getAll()
Mọi key của game cho người chơi này, gồm cả giá trị đang chờ gửi.
Giới hạn và hành vi
- Key là chuỗi không rỗng, tối đa 128 ký tự. Giá trị phải chuyển được sang JSON: SDK chuyển qua JSON trước khi gửi (hàm bị bỏ,
Datethành chuỗi);undefined,BigInthay object vòng lặp bị rejectinvalid_argument. - Portal: dữ liệu nằm trên server, gắn với tài khoản khách (cookie) của người chơi; xoá cookie hoặc đổi thiết bị là một người chơi mới với kho trống. Tổng JSON mọi key của một game tối đa 100 KB (102 400 byte UTF-8), vượt thì reject
quota_exceeded. Gánnullcho một key nghĩa là xoá key đó. Server cho tối đa 30 lần ghi mỗi phút cho mỗi người chơi, vượt thì rejectrate_limited, nên hãy lưu ở các mốc (hết lượt, đổi cài đặt) chứ đừng lưu mỗi khung hình. Lầnget/getAllđầu tiên tải toàn bộ dữ liệu một lần, các lần sau đọc từ bộ nhớ đệm của phiên. - Standalone: lưu trong
localStoragevới khoáwsg:<gameId>:storage:<key>, cũng giới hạn 100 KB (tính theo độ dài key cộng chuỗi JSON). NếulocalStoragekhông dùng được (chế độ riêng tư, bị chặn), dữ liệu chỉ sống trong bộ nhớ của tab. - Disabled:
gettrảnull,getAlltrả{},setvàremoveresolve nhưng không lưu gì.
await WSG.storage.set({ best: 1200, level: 4, settings: { music: false } });
const settings = (await WSG.storage.get('settings')) ?? { music: true };
await WSG.storage.remove('tutorialStep');Chia sẻ
WSG.share(payload)
share(payload: { text?: string; score?: number; url?: string }): Promise<{ ok: boolean }>;- Portal: trang portal mở bảng chia sẻ của hệ điều hành (
navigator.share) hoặc chép vào clipboard. Nếu không cóurl, portal dùng link trang game trên portal:https://leonogames.com/games/<slug>?ref=share(kèm&score=<score>khi cóscore).texttối đa 280 ký tự vàurlphải là URL tuyệt đối, nếu không Promise rejectinvalid_argument.oklàfalsenếu người chơi huỷ hoặc trình duyệt không hỗ trợ cả hai cách. - Standalone: game tự gọi
navigator.sharehoặc chép clipboard, nội dung làtextcộngScore: <score>. - Disabled:
{ ok: false }. - Gọi ngay trong xử lý chạm hoặc click, vì trình duyệt chỉ cho chia sẻ ngay sau một thao tác của người dùng.
shareButton.addEventListener('click', async () => {
const { ok } = await WSG.share({ text: `Mình được ${score} điểm!`, score });
if (!ok) showToast('Chưa chia sẻ được');
});Ngôn ngữ và tham số
WSG.getLocale()
getLocale(): string;Gọi được trước init(). Trả mã ngôn ngữ 2–3 chữ cái thường. Trong portal (sau init()) là ngôn ngữ giao diện portal, hiện là vi hoặc en. Trước init() và ở standalone: query lang, rồi navigator.language, cuối cùng là en.
WSG.getEntryParams()
getEntryParams(): Record<string, string>;Gọi được trước init(). Trả bản sao các query của URL game, trừ wsg, host, lang, v dành cho portal, gộp với tham số portal chuyển kèm lời bắt tay. Hiện portal chưa chuyển tham số nào; ở chế độ test sẽ có wsg_test. Ở standalone, index.html?level=3 cho { level: '3' }, tiện để mở thẳng một màn khi kiểm thử.
Sự kiện
WSG.on(event, callback) và WSG.off(event, callback)
on<K extends keyof WSGEventMap>(event: K, cb: (...args: WSGEventMap[K]) => void): () => void;
off(event: string, cb: (...args: never[]) => void): void;
interface WSGEventMap {
pause: [];
resume: [];
audio: [enabled: boolean];
authchange: [player: Player];
visibility: [visible: boolean];
}Gọi được trước init(). on() trả về hàm huỷ đăng ký. Lỗi ném ra trong callback được SDK bắt và in ra console, không làm hỏng SDK.
| Sự kiện | Tham số | Khi nào |
|---|---|---|
pause | — | Tab bị ẩn; trước khi quảng cáo hiện |
resume | — | Tab hiện lại; sau khi quảng cáo đóng |
audio | enabled | false trước quảng cáo, true sau quảng cáo |
visibility | visible | Trang đổi trạng thái hiển thị (document.visibilitychange) |
authchange | player | Thông tin người chơi thay đổi |
pause/resumevàaudiokhông bao giờ lặp trạng thái: hai lầnpauseliên tiếp chỉ phát một lần.- Trong portal, khi tab bị ẩn game nhận
visibility(false)rồipause(trừ lúc đang hiện quảng cáo, vì game đã được tạm dừng sẵn); tab hiện lại cóvisibility(true)rồiresume. Portal cũng phủ overlay "Đang tạm dừng" lên game. - Ở standalone, SDK tự phát
visibilityvàpause/resumetheodocument.visibilitychange, và quanh quảng cáo giả, để bạn kiểm thử giống trong portal.
const offPause = WSG.on('pause', () => loop.stop());
WSG.on('resume', () => loop.start());
WSG.on('audio', (enabled) => (sound.muted = !enabled));
WSG.on('authchange', (player) => (nameLabel.text = player.name));
// ...
offPause(); // huỷ một listenerLỗi
Mã lỗi
Mọi Promise bị reject đều nhận một WSGError: là Error thật (có stack), có thêm code và message; JSON.stringify(err) cho { code, message }. Bản script tag không xuất class WSGError, hãy kiểm tra err.code.
code | Khi nào |
|---|---|
not_initialized | Gọi API bất đồng bộ trước khi init() xong; trong portal cũng gặp khi phiên chơi đã kết thúc |
timeout | Portal không trả lời đúng hạn: 10 giây cho đa số yêu cầu (init có 3 giây nhưng hết hạn thì rơi về standalone, không reject) |
quota_exceeded | Storage vượt 100 KB |
invalid_argument | Tham số sai: gameId, period, key hay giá trị storage, score không phải số hữu hạn, meta không phải object; hoặc portal từ chối dữ liệu (ví dụ text chia sẻ dài quá 280 ký tự) |
rate_limited | Vượt giới hạn của API portal, ví dụ ghi storage quá 30 lần mỗi phút |
unavailable | Không gửi được tới portal, kênh đã đóng, hoặc portal không gọi được API (mất mạng, bị từ chối quyền) |
internal | Lỗi khác, gồm mã lạ do portal trả về |
Quảng cáo không reject vì sự cố (trả reason), và điểm bị từ chối cũng không reject (trả accepted: false). Bọc try/catch cho storage và các lời gọi bạn cần biết thất bại:
try {
await WSG.storage.set('save', bigSaveObject);
} catch (err) {
if (err.code === 'quota_exceeded') trimOldSaves();
else console.warn('Không lưu được', err.code, err.message);
}Chế độ standalone và disabled
| API | Standalone | Disabled |
|---|---|---|
player | local-<id>, tên Guest | Người chơi portal gửi kèm (nếu có), không thì mặc định |
getToken() | null | null |
isAvailable() | true trừ auth | false |
Vòng đời, happyTime, track | Không gửi đi (track chỉ in khi debug) | Không gửi đi |
ads.* | Chờ 1 giây, shown: true (rewarded có rewarded: true) | { shown: false, reason: 'disabled' } |
leaderboard.submit | Luôn nhận, lưu localStorage | accepted: false, reason: 'disabled' |
top, around, me | Bảng cục bộ | [], [], null |
show | Không làm gì | Không làm gì |
storage.* | localStorage (100 KB) | Không lưu, get trả null |
share | navigator.share hoặc clipboard | { ok: false } |
| Sự kiện | pause/resume/visibility theo tab và quảng cáo giả | Vẫn nhận sự kiện từ portal |
Kiểu TypeScript
Nếu game viết bằng TypeScript và nhúng SDK bằng thẻ script, thêm file khai báo này vào dự án (kiểu khớp với SDK v1):
export {};
declare global {
type WSGEnvironment = 'portal' | 'standalone' | 'disabled';
type WSGPeriod = 'daily' | 'weekly' | 'all';
type WSGFeature = 'ads.interstitial' | 'ads.rewarded' | 'leaderboard' | 'storage' | 'share' | 'auth';
type WSGAdReason =
'cooldown' | 'unfilled' | 'adblock' | 'disabled' | 'dismissed' | 'not_available' | 'error';
type WSGErrorCode =
| 'not_initialized'
| 'timeout'
| 'quota_exceeded'
| 'invalid_argument'
| 'rate_limited'
| 'unavailable'
| 'internal';
interface WSGPlayerInfo {
id: string;
name: string;
avatarUrl: string | null;
isGuest: boolean;
}
interface WSGInitOptions {
gameId: string;
version?: string;
debug?: boolean;
}
interface WSGInitContext {
environment: WSGEnvironment;
player: WSGPlayerInfo;
locale: string;
features: Record<WSGFeature, boolean>;
entryParams: Record<string, string>;
}
interface WSGAdResult {
shown: boolean;
rewarded?: boolean;
reason?: WSGAdReason;
}
interface WSGLeaderboardEntry {
rank: number;
playerId: string;
name: string;
avatarUrl: string | null;
score: number;
isMe: boolean;
}
interface WSGSubmitResult {
accepted: boolean;
best: number;
rank: number | null;
improved: boolean;
reason?: string;
}
interface WSGError extends Error {
code: WSGErrorCode;
}
interface WSGEventMap {
pause: [];
resume: [];
audio: [enabled: boolean];
authchange: [player: WSGPlayerInfo];
visibility: [visible: boolean];
}
interface WSGStatic {
readonly version: string;
readonly environment: WSGEnvironment;
init(options: WSGInitOptions): Promise<WSGInitContext>;
isAvailable(feature: WSGFeature): boolean;
player: WSGPlayerInfo & {
getToken(): Promise<string | null>;
login(): Promise<WSGPlayerInfo>;
};
loadingStart(): void;
loadingProgress(percent: number): void;
loadingFinished(): void;
gameplayStart(): void;
gameplayStop(): void;
happyTime(intensity?: number): void;
track(name: string, props?: Record<string, unknown>): void;
ads: {
interstitial(opts?: { placement?: string }): Promise<WSGAdResult>;
rewarded(opts?: { placement?: string }): Promise<WSGAdResult>;
isBlocked(): boolean;
};
leaderboard: {
submit(score: number, meta?: Record<string, unknown>): Promise<WSGSubmitResult>;
top(opts?: { period?: WSGPeriod; limit?: number }): Promise<WSGLeaderboardEntry[]>;
around(opts?: { period?: WSGPeriod; radius?: number }): Promise<WSGLeaderboardEntry[]>;
me(period?: WSGPeriod): Promise<WSGLeaderboardEntry | null>;
show(period?: WSGPeriod): Promise<void>;
};
storage: {
get<T = unknown>(key: string): Promise<T | null>;
set(key: string, value: unknown): Promise<void>;
set(entries: Record<string, unknown>): Promise<void>;
remove(key: string): Promise<void>;
getAll(): Promise<Record<string, unknown>>;
};
share(payload: { text?: string; score?: number; url?: string }): Promise<{ ok: boolean }>;
getLocale(): string;
getEntryParams(): Record<string, string>;
on<K extends keyof WSGEventMap>(event: K, cb: (...args: WSGEventMap[K]) => void): () => void;
off(event: string, cb: (...args: never[]) => void): void;
}
var WSG: WSGStatic;
}