Bỏ qua tới nội dung
Các trang tài liệu

Nhà phát triển · Tham chiếu

Tham chiếu SDK

Mọi API của SDK v1 — chữ ký TypeScript, hành vi trong portal và khi chạy standalone, giới hạn, sự kiện và mã lỗi.

Trên trang này

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

APITrả vềGọi trước init()
WSG.version, WSG.environmentstring, EnvironmentĐược
WSG.init(options)Promise<InitContext>—
WSG.isAvailable(feature)booleanTrả false
WSG.player, getToken(), login()Player, Promise<string | null>, Promise<Player>Hàm bị từ chối
loadingStart(), loadingProgress(), loadingFinished()voidBị bỏ qua
gameplayStart(), gameplayStop()voidBị bỏ qua
happyTime(intensity?)voidBị bỏ qua
track(name, props?)voidBị bỏ qua
ads.interstitial(), ads.rewarded(), ads.isBlocked()Promise<AdResult>, booleanBị 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:

HTML
<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

TypeScript
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

TypeScript
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àoHành vi
portalGame nằm trong iframe, URL có wsg=1 và host=<origin http(s)>, và portal trả lời bắt tay trong 3 giâyMọ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
standaloneMở game trực tiếp, thiếu query, hoặc portal không trả lời trong 3 giâyChạ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
disabledPortal 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)

TypeScript
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>;
}
  • gameId bắ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ới invalid_argument. Dùng đúng slug của game. Portal so gameId vớ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: true in 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ưa init, rơi về standalone vì portal không trả lời, track sai 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 sang standalone (bật debug sẽ thấy cảnh báo).
  • ctx.player là bản sao người chơi lúc khởi tạo; muốn đọc giá trị mới nhất, dùng WSG.player.
  • ctx.locale: trong portal là ngôn ngữ giao diện portal (vi hoặc en); ở standalone lấy từ query lang, rồi navigator.language, cuối cùng là en. Luôn là mã 2–3 chữ cái thường.
  • ctx.features: xem isAvailable(). ctx.entryParams: xem getEntryParams().
JavaScript
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)

TypeScript
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 features của game.json ở phiên bản đang chạy và đội vận hành đang bật nó cho game trong trang admin. share không có trong game.json nên chỉ theo công tắc admin; auth luôn false vì portal chưa có đăng nhập.
  • Standalone: mọi tính năng true trừ 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".

JavaScript
if (WSG.isAvailable('ads.rewarded')) showReviveButton();

Người chơi

WSG.player

TypeScript
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: id là mã người chơi (UUID) trên portal, name là tên hiển thị (người chơi tự đổi được ở portal), isGuest là true với tài khoản khách. Bản v1 chỉ có tài khoản khách.
  • Standalone: id dạng local-<12 ký tự>, giữ trong localStorage (khoá wsg:local-id), name là Guest.
  • Các trường được cập nhật tại chỗ khi có sự kiện authchange, nên đọc WSG.player.name lúc cần thay vì lưu lại một lần.

WSG.player.getToken()

TypeScript
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()

TypeScript
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()

TypeScript
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ện load_timeout.
  • Portal ghi các sự kiện load_start, load_end, và load_progress mỗ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()

TypeScript
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 khi init() xong bị bỏ qua, và điểm nộp sau đó sẽ bị từ chối với no_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ại gameplayStart().
  • Đ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?)

TypeScript
happyTime(intensity?: number): void; // 0..1, mặc định 1

Bá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?)

TypeScript
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.

  • name khớ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ằng wsg_ 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ột track('gameplay_start') được coi là gameplay bắt đầu).
  • props là object JSON, tối đa 2048 byte UTF-8 sau khi JSON.stringify. SDK chuyển qua JSON trước khi gửi: hàm và undefined bị bỏ, Date thà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ối no_gameplay. Hãy giữ ở mức vài sự kiện cho mỗi lượt chơi.
JavaScript
WSG.track('level_complete', { level: 3, stars: 2, timeMs: 41250 });

Quảng cáo

WSG.ads.interstitial(opts?)

TypeScript
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?)

TypeScript
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()

TypeScript
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

TypeScript
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:

  1. Nếu gameplay đang chạy, SDK gọi gameplayStop().
  2. SDK phát pause và audio(false).
  3. Portal hiện quảng cáo (hoặc trả lời ngay với một reason).
  4. SDK phát audio(true) và resume, rồi Promise resolve.
reasonÝ nghĩaGame nên làm
(không có)Quảng cáo đã hiệnInterstitial: chơi tiếp. Rewarded: xem rewarded
cooldownChư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ácKhông gọi chồng; bỏ qua
dismissedNgười chơi đóng rewarded trước khi xem hết (shown: true, rewarded: false)Không cấp thưởng
unfilledKhông có quảng cáo để hiệnBỏ qua; với rewarded có thể ẩn nút một lúc
adblockTrình chặn quảng cáo; isBlocked() thành trueẨn nút gắn với quảng cáo
disabledSDK ở chế độ disabledBỏ qua
errorLỗi môi trường, ví dụ portal không trả lời trong 120 giâyBỏ 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

TypeScript
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?)

TypeScript
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
}
  • score phải là số hữu hạn, nếu không Promise reject invalid_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ới reason: 'invalid_score'. Làm tròn trước khi nộp.
  • meta là 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, reason là một trong invalid_session, invalid_score, rate_limited, no_gameplay, over_max, too_fast, rate_too_high, blocked; best và rank là trạng thái hiện có (0 và null nếu chưa có điểm); improved: false.
  • Với game scoreOrder: "asc" (thấp hơn là tốt hơn), best là đ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; rank trong kết quả là hạng mọi lúc.
  • Bật debug sẽ 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.

JavaScript
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?)

TypeScript
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?)

TypeScript
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?)

TypeScript
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?)

TypeScript
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

TypeScript
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ỏ, Date thành chuỗi); undefined, BigInt hay object vòng lặp bị reject invalid_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án null cho 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ì reject rate_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ần get/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 localStorage vớ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ếu localStorage không dùng được (chế độ riêng tư, bị chặn), dữ liệu chỉ sống trong bộ nhớ của tab.
  • Disabled: get trả null, getAll trả {}, set và remove resolve nhưng không lưu gì.
JavaScript
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)

TypeScript
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). text tối đa 280 ký tự và url phải là URL tuyệt đối, nếu không Promise reject invalid_argument. ok là false nế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.share hoặc chép clipboard, nội dung là text cộng Score: <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.
JavaScript
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()

TypeScript
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()

TypeScript
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)

TypeScript
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ệnTham 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
audioenabledfalse trước quảng cáo, true sau quảng cáo
visibilityvisibleTrang đổi trạng thái hiển thị (document.visibilitychange)
authchangeplayerThông tin người chơi thay đổi
  • pause/resume và audio không bao giờ lặp trạng thái: hai lần pause liên tiếp chỉ phát một lần.
  • Trong portal, khi tab bị ẩn game nhận visibility(false) rồi pause (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ồi resume. Portal cũng phủ overlay "Đang tạm dừng" lên game.
  • Ở standalone, SDK tự phát visibility và pause/resume theo document.visibilitychange, và quanh quảng cáo giả, để bạn kiểm thử giống trong portal.
JavaScript
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 listener

Lỗ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.

codeKhi nào
not_initializedGọ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
timeoutPortal 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_exceededStorage vượt 100 KB
invalid_argumentTham 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_limitedVượt giới hạn của API portal, ví dụ ghi storage quá 30 lần mỗi phút
unavailableKhô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)
internalLỗ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:

JavaScript
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

APIStandaloneDisabled
playerlocal-<id>, tên GuestNgười chơi portal gửi kèm (nếu có), không thì mặc định
getToken()nullnull
isAvailable()true trừ authfalse
Vòng đời, happyTime, trackKhô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.submitLuôn nhận, lưu localStorageaccepted: false, reason: 'disabled'
top, around, meBảng cục bộ[], [], null
showKhông làm gìKhông làm gì
storage.*localStorage (100 KB)Không lưu, get trả null
sharenavigator.share hoặc clipboard{ ok: false }
Sự kiệnpause/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):

TypeScriptwsg.d.ts
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;
}