@virjay/playscope-cocos 0.0.0-stage → 0.2.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +314 -2
  3. package/USAGE.md +372 -0
  4. package/VALIDATION.md +93 -0
  5. package/dist/adapters/levelplay/index.d.ts +15 -0
  6. package/dist/adapters/levelplay/index.js +14 -0
  7. package/dist/cocos/Storage.d.ts +16 -0
  8. package/dist/cocos/Storage.js +16 -0
  9. package/dist/cocos/XhrJsonTransport.d.ts +8 -0
  10. package/dist/cocos/XhrJsonTransport.js +65 -0
  11. package/dist/cocos/index.d.ts +48 -0
  12. package/dist/cocos/index.js +134 -0
  13. package/dist/core/ClientOwnership.d.ts +6 -0
  14. package/dist/core/ClientOwnership.js +39 -0
  15. package/dist/core/ClientScope.d.ts +5 -0
  16. package/dist/core/ClientScope.js +34 -0
  17. package/dist/core/ConfigurationSync.d.ts +18 -0
  18. package/dist/core/ConfigurationSync.js +66 -0
  19. package/dist/core/FactEncoding.d.ts +10 -0
  20. package/dist/core/FactEncoding.js +29 -0
  21. package/dist/core/HttpError.d.ts +10 -0
  22. package/dist/core/HttpError.js +17 -0
  23. package/dist/core/OutboxStore.d.ts +31 -0
  24. package/dist/core/OutboxStore.js +228 -0
  25. package/dist/core/PersistentState.d.ts +33 -0
  26. package/dist/core/PersistentState.js +92 -0
  27. package/dist/core/PlayScopeClient.d.ts +65 -0
  28. package/dist/core/PlayScopeClient.js +422 -0
  29. package/dist/core/QueueAdmission.d.ts +4 -0
  30. package/dist/core/QueueAdmission.js +29 -0
  31. package/dist/core/index.d.ts +7 -0
  32. package/dist/core/index.js +27 -0
  33. package/dist/core/revenue.d.ts +33 -0
  34. package/dist/core/revenue.js +23 -0
  35. package/dist/core/types.d.ts +155 -0
  36. package/dist/core/types.js +2 -0
  37. package/dist/core/uuid.d.ts +1 -0
  38. package/dist/core/uuid.js +9 -0
  39. package/dist/esm/adapters/levelplay/index.js +11 -0
  40. package/dist/esm/cocos/Storage.js +12 -0
  41. package/dist/esm/cocos/XhrJsonTransport.js +61 -0
  42. package/dist/esm/cocos/index.js +115 -0
  43. package/dist/esm/core/ClientOwnership.js +34 -0
  44. package/dist/esm/core/ClientScope.js +30 -0
  45. package/dist/esm/core/ConfigurationSync.js +62 -0
  46. package/dist/esm/core/FactEncoding.js +24 -0
  47. package/dist/esm/core/HttpError.js +12 -0
  48. package/dist/esm/core/OutboxStore.js +224 -0
  49. package/dist/esm/core/PersistentState.js +85 -0
  50. package/dist/esm/core/PlayScopeClient.js +415 -0
  51. package/dist/esm/core/QueueAdmission.js +25 -0
  52. package/dist/esm/core/index.js +7 -0
  53. package/dist/esm/core/revenue.js +20 -0
  54. package/dist/esm/core/types.js +1 -0
  55. package/dist/esm/core/uuid.js +6 -0
  56. package/dist/esm/package.json +1 -0
  57. package/dist/esm/protocols/game-v1/index.js +21 -0
  58. package/dist/protocols/game-v1/index.d.ts +8 -0
  59. package/dist/protocols/game-v1/index.js +25 -0
  60. package/doc/README.md +4 -0
  61. package/doc//344/275/277/347/224/250/346/214/207/345/215/227/README.md +585 -0
  62. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/adapters/levelplay/index.d.ts +15 -0
  63. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/cocos/Storage.d.ts +13 -0
  64. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/cocos/XhrJsonTransport.d.ts +8 -0
  65. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/cocos/index.d.ts +38 -0
  66. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/core/PlayScopeClient.d.ts +46 -0
  67. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/core/index.d.ts +3 -0
  68. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/core/revenue.d.ts +25 -0
  69. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/0.1.1-types/core/types.d.ts +81 -0
  70. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/exports.json +89 -0
  71. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231/public-api.d.ts +281 -0
  72. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/client-context.schema.json +103 -0
  73. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/game-ad-properties.json +765 -0
  74. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/game-builtin-properties.json +479 -0
  75. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/game-event.json +278 -0
  76. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/product-events.json +2878 -0
  77. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257/standard-contract.json +183 -0
  78. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/234/215/345/212/241/347/253/257//344/272/247/345/223/201/344/272/213/344/273/266/347/264/242/345/274/225.md +87 -0
  79. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//345/217/202/350/200/203/350/265/204/346/226/231//346/235/245/346/272/220/344/270/216/347/211/210/346/234/254.json +35 -0
  80. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/CoreIntegration.ts +13 -0
  81. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/LevelPlayTelemetry.ts +119 -0
  82. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/PlayScopeExample.ts +46 -0
  83. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/QuestGarden.ts +129 -0
  84. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/README.md +23 -0
  85. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/project-fixture.json +188 -0
  86. package/doc//344/275/277/347/224/250/346/214/207/345/215/227//347/244/272/344/276/213/tsconfig.json +20 -0
  87. package/examples/cocos-minimal/PlayScopeExample.ts +27 -0
  88. package/examples/quest-garden/QuestGarden.ts +129 -0
  89. package/examples/quest-garden/README.md +180 -0
  90. package/examples/quest-garden/project-fixture.json +188 -0
  91. package/examples/quest-garden/tsconfig.json +12 -0
  92. package/package.json +73 -4
@@ -0,0 +1,183 @@
1
+ {
2
+ "version": 1,
3
+ "id": "standard",
4
+ "description": "通用对局契约:game-event 信封、内置平台事件与产品字典。",
5
+ "envelope": {
6
+ "document": "game-event.json"
7
+ },
8
+ "summary": {
9
+ "type": "object",
10
+ "properties": {
11
+ "score": {
12
+ "type": "number",
13
+ "minimum": -1000000000000,
14
+ "maximum": 1000000000000
15
+ },
16
+ "durationMs": {
17
+ "type": "integer",
18
+ "minimum": 0,
19
+ "maximum": 7776000000
20
+ },
21
+ "metrics": {
22
+ "type": "object",
23
+ "additionalProperties": {
24
+ "type": "number"
25
+ }
26
+ }
27
+ },
28
+ "additionalProperties": false
29
+ },
30
+ "platformSchemas": "builtin",
31
+ "adPlacementOverrides": true,
32
+ "platform": [
33
+ "app_first_open",
34
+ "app_foreground",
35
+ "app_background",
36
+ "screen_view",
37
+ "activity_heartbeat",
38
+ "run_created",
39
+ "run_started",
40
+ "run_paused",
41
+ "run_resumed",
42
+ "run_checkpoint",
43
+ "run_failed",
44
+ "run_revived",
45
+ "run_ended",
46
+ "config_received"
47
+ ],
48
+ "activity": [
49
+ "app_foreground",
50
+ "activity_heartbeat",
51
+ "run_started"
52
+ ],
53
+ "lifecycle": {
54
+ "game_start": "run_started",
55
+ "game_pause": "run_paused",
56
+ "game_resume": "run_resumed",
57
+ "game_over": "run_failed",
58
+ "revive_success": "run_revived",
59
+ "level_win": "run_ended",
60
+ "level_lose": "run_ended"
61
+ },
62
+ "checks": [
63
+ "heartbeat"
64
+ ],
65
+ "capabilities": {
66
+ "standardGameCards": true
67
+ },
68
+ "run": {
69
+ "anyOf": [
70
+ {
71
+ "type": "null"
72
+ },
73
+ {
74
+ "type": "object",
75
+ "properties": {
76
+ "config": {
77
+ "$ref": "#/$defs/Attribution"
78
+ }
79
+ }
80
+ }
81
+ ],
82
+ "$defs": {
83
+ "Attribution": {
84
+ "type": "object",
85
+ "properties": {
86
+ "source": {
87
+ "enum": [
88
+ "bundled",
89
+ "cache",
90
+ "remote"
91
+ ]
92
+ },
93
+ "assignmentId": {
94
+ "type": [
95
+ "string",
96
+ "null"
97
+ ]
98
+ },
99
+ "experimentId": {
100
+ "type": [
101
+ "string",
102
+ "null"
103
+ ]
104
+ },
105
+ "variantId": {
106
+ "type": [
107
+ "string",
108
+ "null"
109
+ ]
110
+ }
111
+ },
112
+ "allOf": [
113
+ {
114
+ "if": {
115
+ "required": [
116
+ "experimentId"
117
+ ],
118
+ "properties": {
119
+ "experimentId": {
120
+ "type": "string"
121
+ }
122
+ }
123
+ },
124
+ "then": {
125
+ "required": [
126
+ "variantId"
127
+ ],
128
+ "properties": {
129
+ "variantId": {
130
+ "type": "string"
131
+ }
132
+ }
133
+ }
134
+ },
135
+ {
136
+ "if": {
137
+ "required": [
138
+ "variantId"
139
+ ],
140
+ "properties": {
141
+ "variantId": {
142
+ "type": "string"
143
+ }
144
+ }
145
+ },
146
+ "then": {
147
+ "required": [
148
+ "experimentId"
149
+ ],
150
+ "properties": {
151
+ "experimentId": {
152
+ "type": "string"
153
+ }
154
+ }
155
+ }
156
+ },
157
+ {
158
+ "if": {
159
+ "required": [
160
+ "source"
161
+ ],
162
+ "properties": {
163
+ "source": {
164
+ "const": "bundled"
165
+ }
166
+ }
167
+ },
168
+ "then": {
169
+ "properties": {
170
+ "assignmentId": {
171
+ "type": "null"
172
+ },
173
+ "experimentId": {
174
+ "type": "null"
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ]
180
+ }
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,87 @@
1
+ # 产品事件索引(80 项)
2
+
3
+ 从随附 product-events.json 摘录。required 仅列 properties 必填字段;run、项目覆盖和额外语义见事件契约章节。
4
+ 原始 source/notes 中的 MAX 字样是产品历史描述,不限制 LevelPlay 使用,也不代表该厂商一定提供对应回调。
5
+
6
+ | 编号 | 事件 | 触发时机 | 必填 properties | 活跃 |
7
+ | --- | --- | --- | --- | --- |
8
+ | E001 | app_install | 首次安装启动 | 无 | False |
9
+ | E002 | app_launch | 冷启动或热启动 | 无 | True |
10
+ | E003 | session_start | 前后台切换进入前台 | 无 | True |
11
+ | E004 | session_end | 进入后台 | 无 | False |
12
+ | E005 | user_first_open | 新设备首次启动 | 无 | True |
13
+ | E006 | user_return | 流失 ≥7 天后再次启动 | 无 | True |
14
+ | E010 | user_login | 登录成功 | 无 | True |
15
+ | E011 | user_register | 完成注册 | 无 | True |
16
+ | E020 | game_start | 点击「开始」进入一局 | 无 | True |
17
+ | E021 | game_pause | 局中点击暂停 | 无 | True |
18
+ | E022 | game_resume | 从暂停恢复 | 无 | True |
19
+ | E023 | game_over | 对局结束(死亡判定) | failureIndex | True |
20
+ | E024 | board_cant_place | 无解探测(出现死局) | 无 | True |
21
+ | E025 | new_high_score | 本局打破个人纪录 | 无 | True |
22
+ | E026 | combo_milestone | 达到连击里程碑(2/3/4/5x) | 无 | True |
23
+ | E027 | level_win | 关卡模式通关 | 无 | True |
24
+ | E028 | level_lose | 关卡模式失败 | 无 | True |
25
+ | E030 | revive_offer_show | 死亡时弹出复活视频选项 | 无 | False |
26
+ | E031 | revive_video_start | 用户点击「看视频复活」 | 无 | True |
27
+ | E032 | revive_video_complete | 激励视频完整播放 | 无 | False |
28
+ | E033 | revive_video_fail | 视频加载失败/中途退出 | 无 | False |
29
+ | E034 | revive_success | 玩家成功复活继续游戏 | failureIndex | True |
30
+ | E035 | revive_block_issued | 复活块生成器下发 | 无 | False |
31
+ | E040 | hint_video_start | 点击「提示/技能」 | 无 | True |
32
+ | E041 | hint_video_complete | 完整播放 | 无 | False |
33
+ | E050 | difficulty_entropy_trigger | 熵增刺客触发 | 无 | False |
34
+ | E051 | difficulty_pity_trigger | 放水保底触发 | 无 | False |
35
+ | E052 | quantile_fetch | 拉取云端分位数 | 无 | False |
36
+ | E060 | ad_request | 业务层调用 loadAd | placement | False |
37
+ | E061 | ad_load_success | 广告加载成功 | 无 | False |
38
+ | E062 | ad_load_fail | 广告加载失败 | 无 | False |
39
+ | E063 | ad_impression | 广告展示(计费点) | 无 | False |
40
+ | E064 | ad_click | 用户点击广告 | 无 | True |
41
+ | E065 | ad_rewarded_grant | 激励视频完整播放发奖 | 无 | False |
42
+ | E066 | ad_skipped | 可跳过广告被跳过 | 无 | False |
43
+ | E067 | ad_close | 广告被关闭 | 无 | False |
44
+ | E120 | mediation_init_success | MAX 聚合 SDK 初始化成功 | sdk_version, country, init_latency_ms | False |
45
+ | E121 | mediation_init_fail | MAX 聚合 SDK 初始化失败 | error_code, error_msg, retry_count | False |
46
+ | E122 | adapter_init | 各广告源 adapter 初始化(AdMob/Meta/Unity 等) | network, network_version, init_status, error_code | False |
47
+ | E123 | ad_request_bid | 本次请求哪些网络参与竞价 | placement, requested_networks, bid_responses, winner_network, bid_price | False |
48
+ | E124 | ad_no_fill | 所有网络都没返回广告 | placement, requested_networks, fill_rate_at_request | False |
49
+ | E125 | ad_request_timeout | 请求超时 | placement, network, timeout_ms, retry | False |
50
+ | E126 | ad_show | 广告成功展示到屏幕(首次曝光) | placement, ad_unit_id, network, ad_format, show_latency_ms | False |
51
+ | E127 | ad_first_quartile | 视频播放 25% | placement, network, ad_unit_id, quartile, played_sec | False |
52
+ | E128 | ad_midpoint | 视频播放 50% | placement, network, ad_unit_id, quartile, played_sec | False |
53
+ | E129 | ad_third_quartile | 视频播放 75% | placement, network, ad_unit_id, quartile, played_sec | False |
54
+ | E130 | ad_complete | 视频播放 100%(完播) | placement, network, ad_unit_id, played_sec, reward_granted | False |
55
+ | E131 | ad_landing_open | 点击后跳转落地页(应用商店/网页) | placement, ad_unit_id, landing_type | True |
56
+ | E132 | ad_landing_return | 从落地页返回 | placement, return_within_sec, action_after_return | True |
57
+ | E133 | ad_revenue | 广告主付费(毛收入) | placement, ad_unit_id, network, revenue, currency, exchange_rate_usd | False |
58
+ | E134 | ad_payout | 平台分成后到手(净收入) | placement, network, gross_revenue, platform_share, net_revenue, currency | False |
59
+ | E135 | ad_ecpm | 千次曝光收入 | placement, network, country, ecpm_usd, impressions, ad_count | False |
60
+ | E136 | ad_freq_cap_hit | 频控命中(被拦截,未展示) | placement, user_id, cap_type, cap_value, hit_count | False |
61
+ | E104 | mail_claim | 领取附件 | message_id, user_id, item, count, claim_latency_sec | True |
62
+ | E105 | mail_expire | 附件过期未领 | message_id, user_id, item, lost_value, expire_at | False |
63
+ | E106 | popup_show | 弹窗出现 | popup_id, ab_version, session_count, force_show | False |
64
+ | E107 | popup_btn_click | 弹窗按钮点击 | popup_id, btn_text, btn_action, click_position | True |
65
+ | E108 | red_dot_show | 红点出现 | entry, count, source | False |
66
+ | E109 | red_dot_click | 红点点击 | entry, count_before, count_after, clear_method | True |
67
+ | E110 | stamina_change | 体力增减 | user_id, change_type, delta, before, after, reason | False |
68
+ | E111 | stamina_overflow | 体力溢出 | user_id, overflow_amount, recover_at, max_stamina | False |
69
+ | E070 | config_fetch | 从 Remote Config 拉取配置 | user_id, config_version, fetch_latency_ms, ab_group, source | False |
70
+ | E071 | config_apply | 配置生效(参数已应用到本地) | user_id, config_version, applied_params_count, diff_params | False |
71
+ | E080 | client_error | 客户端业务异常 | error_type, error_msg, stack_trace, page, user_action | False |
72
+ | E081 | sdk_error | 第三方 SDK 报错 | sdk_name, error_code, error_msg, retry_count, network | False |
73
+ | E082 | crash | 客户端崩溃 | stack_trace, device, os, app_version, is_recoverable | False |
74
+ | E083 | cheat_signal | 作弊/外挂信号 | cheat_type, signal_strength, device_id, action | False |
75
+ | E090 | push_token_register | 设备首次注册推送 token | push_id, user_id, token, channel, registered_at | False |
76
+ | E091 | push_token_refresh | token 刷新(换设备/重置/过期) | push_id, user_id, old_token, new_token, channel, refreshed_at | False |
77
+ | E092 | push_send | 后台发起一次推送任务 | push_id, target_segment, target_size, channel, copy_id, ab_group, scheduled_at | False |
78
+ | E093 | push_delivered | API 层面送达成功 | push_id, user_id, channel, delivered_at, latency_ms | False |
79
+ | E094 | push_received | SDK 收到推送 | push_id, user_id, received_at, push_type | False |
80
+ | E095 | push_open | 用户点击打开 | push_id, user_id, opened_at, deep_link | True |
81
+ | E096 | push_dismiss | 收到但划掉未点击 | push_id, user_id, dismissed_at, displayed_duration_ms | False |
82
+ | E097 | push_convert | 收到/点击后 24h 内回流 | push_id, user_id, converted_at, return_within_sec, action_after_return | True |
83
+ | E098 | push_channel_fail | 通道失败 | push_id, user_id, channel, error_code, error_msg, retry_count | False |
84
+ | E100 | marquee_show | 跑马灯曝光 | marquee_id, ab_version, country, position, exposure_duration_ms | False |
85
+ | E101 | marquee_click | 跑马灯点击 | marquee_id, link_action, ab_version, click_latency_ms | True |
86
+ | E102 | mail_new | 收到新站内信 | message_id, template_id, user_id, has_attachment | False |
87
+ | E103 | mail_read | 打开站内信 | message_id, user_id, time_to_read_sec | True |
@@ -0,0 +1,35 @@
1
+ {
2
+ "date": "2026-11-12",
3
+ "sdkPackageVersion": "0.2.0",
4
+ "sdkSourceStatus": "0.2.0 distribution source; npm name @virjay/playscope-cocos",
5
+ "serverSourceVersion": "0.1.21",
6
+ "note": "Source snapshots for this SDK distribution; server source version does not prove a rebuilt/released server. Project-specific contracts may differ.",
7
+ "sdkSourceSha256": {
8
+ "core/types.ts": "202e3a01ac32b589c15fd1a78cafcbae525769c7571ec2acf5b213d9ff039f3e",
9
+ "core/PlayScopeClient.ts": "45ec11717248a2a1ee125fab6b04002bc9abf7031dec81f8accdea65d8ce0171",
10
+ "core/ConfigurationSync.ts": "f68f98bb3e1852fb5e0948ad79b7e65433bd82cf56a13e0601f4492183a4b27d",
11
+ "core/revenue.ts": "f35c52f86c376c92147c776f67c350820d55421979bec44c69531352ac36fe42",
12
+ "core/PersistentState.ts": "0225aa806e48704f9eadfe83f221118a8dc7165c00ac12c384bf2983f153141d",
13
+ "core/ClientScope.ts": "218f52c139b019203acabe616cf309d01b7129c37e5c149fccb2876897365bc2",
14
+ "core/ClientOwnership.ts": "0090a14fb1f177233ac7fdefca0c2055135bef0f5919d55014cdc67d745e648a",
15
+ "core/HttpError.ts": "7280dd6445c1f4ad1837f558b18451e3d94c3924f51303e6d76fe1a81e0789f8",
16
+ "core/QueueAdmission.ts": "0f4b2664a6839f78289c35fa11076999acf07c1575a213dc9dbef67364a0a23e",
17
+ "core/uuid.ts": "9606b0561f5ee05b8d2865de38b0a3b198f3ba620f3241b53653f4fe4f3e065c",
18
+ "core/FactEncoding.ts": "1340b10d060eb8a9c853c1192c2798db71dbe20960fdb854d5b8875044f5f016",
19
+ "cocos/index.ts": "d0770451dedf74d88a666ab0b1444bc2146266f919e76f9b3d11c218dce7eb65",
20
+ "cocos/Storage.ts": "c3fa8d586a5cad49329e78c7d1f8ef8c9fe8d663715e31eb8753ea255b67bf21",
21
+ "cocos/XhrJsonTransport.ts": "a36a2516e0e1e02ca76c8a66beaf1b84c21e1fff768ce84d0e934e87b793811a",
22
+ "adapters/levelplay/index.ts": "4e392549557e67e82c6b0a85b50d4edcc43dc866691141ce2a1c75f0792de39d",
23
+ "protocols/game-v1/index.ts": "454c1f485b4004992075bc24ac3c7b86d0f935708d67d161b85800a7608dac8b",
24
+ "examples/quest-garden/QuestGarden.ts": "2e3850ec5b89e47829d51b94afebf5f6b485b19f0244ea4b42e1599a30d89906",
25
+ "examples/quest-garden/project-fixture.json": "84c0a8fe4cfda1c3fd5a6770002c9be7ab25cbf43273b26b45f3d06e25467ba7"
26
+ },
27
+ "serverSourceSha256": {
28
+ "internal/contract/game-event.json": "acf39884ef6ad1bff46d4b44b459f7b156051ef102f06605c9f7bb8cca70a0db",
29
+ "internal/contract/client-context.json": "066767bcc4b6a22d7713539f5cca1cfd91b0ad93e0fe1e73419427d1e623a7ac",
30
+ "internal/contract/product-events.json": "2f261d3ee191783f3660e69cc7c5d06cd714fc649285bdb466caf25894dbce96",
31
+ "internal/contract/seeds/standard.json": "69595310293eb23d043c1ed65c6d2ecdd22f6bd78b0e6c94fcee528038f8db86",
32
+ "internal/contract/game.go": "822a706fd15dc4060dd38b9a16fb3e98fc59591d460a7ddac0aa39f28b0418a8",
33
+ "internal/contract/product.go": "006292482da541621fddb2aff9e5266273f7caeb7716acb3e7680dfc51464f02"
34
+ }
35
+ }
@@ -0,0 +1,13 @@
1
+ import { PlayScopeClient, type PlayScopeOptions, type StoragePort, type JsonTransport } from '@virjay/playscope-cocos';
2
+
3
+ /** Existing host owns its tick/lifecycle. Do not also construct CocosPlayScope for this store/key. */
4
+ export function createCore(options: PlayScopeOptions, storage: StoragePort, transport: JsonTransport,
5
+ collectionAllowed: boolean): PlayScopeClient | null {
6
+ if (!collectionAllowed) return null;
7
+ const client = new PlayScopeClient(options, storage, transport);
8
+ client.start();
9
+ void client.flush();
10
+ return client;
11
+ }
12
+ // Host must call client.flush() from its existing schedule and client.dispose() when releasing it.
13
+ // Supply a dedicated transport instance; dispose can abort requests owned by that transport.
@@ -0,0 +1,119 @@
1
+ import { telemetryUuid, type CocosPlayScope, type AdRevenueFact } from '@virjay/playscope-cocos';
2
+ import { forwardLevelPlayRevenue, type LevelPlayImpression } from '@virjay/playscope-cocos/adapters/levelplay';
3
+
4
+ /** Documentation host adapter. Native SDK initialization/callback binding remains with the game. */
5
+ export interface AdRequestSnapshot {
6
+ requestId: string;
7
+ placement: AdRevenueFact['placement'];
8
+ format: AdRevenueFact['format'];
9
+ isTest: boolean;
10
+ isMock?: boolean;
11
+ isDemo?: boolean;
12
+ adConfigRevision?: string;
13
+ run: unknown;
14
+ }
15
+ export interface AdExposureSnapshot extends AdRequestSnapshot {
16
+ exposureId: string;
17
+ shownAt: string;
18
+ network: string;
19
+ adUnitId: string;
20
+ country?: string;
21
+ }
22
+ export interface RewardOutcome {
23
+ outcome: 'granted' | 'not_granted' | 'failed' | 'unknown' | 'demo';
24
+ persistence: 'saved' | 'pending' | 'not_required' | 'unknown';
25
+ reason?: string;
26
+ }
27
+ const copy = <T>(value: T): T => JSON.parse(JSON.stringify(value));
28
+
29
+ export class LevelPlayTelemetry {
30
+ constructor(private readonly telemetry: CocosPlayScope) {}
31
+
32
+ createRequest(input: Omit<AdRequestSnapshot, 'requestId'>): AdRequestSnapshot {
33
+ return copy({ ...input, requestId: telemetryUuid() });
34
+ }
35
+
36
+ /** Call once per native display/auction. Reuse the result for repeated/late callbacks. */
37
+ captureExposure(request: AdRequestSnapshot, display: {
38
+ exposureId: string; shownAt: string; network: string; adUnitId: string; country?: string;
39
+ }): AdExposureSnapshot {
40
+ return copy({ ...request, ...display });
41
+ }
42
+
43
+ recordRequest(request: AdRequestSnapshot): boolean {
44
+ return this.once('ad_request', this.properties(request), request.run, request.requestId);
45
+ }
46
+ recordLoaded(request: AdRequestSnapshot, cacheHit: boolean): boolean {
47
+ return this.once('ad_load_success', { ...this.properties(request), cache_hit: cacheHit }, request.run, request.requestId);
48
+ }
49
+ recordLoadFailure(request: AdRequestSnapshot, code: string): boolean {
50
+ return this.once('ad_load_fail', { ...this.properties(request), error_code: code }, request.run, request.requestId);
51
+ }
52
+ recordDisplay(exposure: AdExposureSnapshot, latencyMs: number): { shown: boolean; impression: boolean } {
53
+ if (!Number.isSafeInteger(latencyMs) || latencyMs < 0) throw Error('invalid_display_latency');
54
+ const properties = this.properties(exposure);
55
+ return {
56
+ shown: this.once('ad_show', { ...properties, show_latency_ms: latencyMs },
57
+ exposure.run, exposure.exposureId, exposure.shownAt),
58
+ impression: this.once('ad_impression', properties, exposure.run, exposure.exposureId, exposure.shownAt),
59
+ };
60
+ }
61
+ recordClick(exposure: AdExposureSnapshot): boolean { return this.exposureEvent('ad_click', exposure); }
62
+ recordClose(exposure: AdExposureSnapshot): boolean { return this.exposureEvent('ad_close', exposure); }
63
+ recordSkipped(exposure: AdExposureSnapshot): boolean { return this.exposureEvent('ad_skipped', exposure); }
64
+
65
+ recordRevenue(exposure: AdExposureSnapshot, impression: LevelPlayImpression): boolean {
66
+ // Do not independently track ad_revenue as well: this helper already generates it.
67
+ return forwardLevelPlayRevenue(this.telemetry, impression, {
68
+ exposureId: exposure.exposureId, placement: exposure.placement,
69
+ businessPlacement: exposure.placement, format: exposure.format, isTest: exposure.isTest,
70
+ isMock: exposure.isMock, isDemo: exposure.isDemo, shownAt: exposure.shownAt,
71
+ adConfigRevision: exposure.adConfigRevision,
72
+ }, { run: exposure.run });
73
+ }
74
+ /** Call after the game has determined the actual reward and persistence outcome. */
75
+ recordRewardOutcome(exposure: AdExposureSnapshot, reward: RewardOutcome): boolean {
76
+ if (exposure.format !== 'rewarded') return false;
77
+ return this.once('ad_rewarded_grant', { ...this.properties(exposure), sdk_reward_received: true,
78
+ reward_outcome: reward.outcome, reward_persistence: reward.persistence,
79
+ ...(reward.reason ? { reward_reason: reward.reason } : {}),
80
+ }, exposure.run, exposure.exposureId);
81
+ }
82
+ recordDisplayFailure(request: AdRequestSnapshot, code: string, retryCount: number, network: string): boolean {
83
+ return this.once('sdk_error', { ...this.properties(request), sdk_name: 'levelplay', error_code: code,
84
+ error_msg: 'LevelPlay display failed', retry_count: retryCount, network, phase: 'display',
85
+ }, request.run, request.requestId);
86
+ }
87
+ /** Use only if the target native SDK actually supplies the matching playback callback. */
88
+ recordVideoQuartile(exposure: AdExposureSnapshot, quartile: 25 | 50 | 75, playedSec: number): boolean {
89
+ if (exposure.format !== 'rewarded' || !Number.isFinite(playedSec) || playedSec < 0) return false;
90
+ const names = { 25: 'ad_first_quartile', 50: 'ad_midpoint', 75: 'ad_third_quartile' } as const;
91
+ return this.once(names[quartile], { ...this.properties(exposure), quartile, played_sec: playedSec },
92
+ exposure.run, exposure.exposureId);
93
+ }
94
+ /** Do not call this from reward/close callbacks unless there is independent completion evidence. */
95
+ recordVideoComplete(exposure: AdExposureSnapshot, playedSec: number, rewardGranted: boolean): boolean {
96
+ if (exposure.format !== 'rewarded' || !Number.isFinite(playedSec) || playedSec < 0) return false;
97
+ return this.once('ad_complete', { ...this.properties(exposure), played_sec: playedSec,
98
+ reward_granted: rewardGranted ? 'yes' : 'no' }, exposure.run, exposure.exposureId);
99
+ }
100
+ private exposureEvent(name: string, exposure: AdExposureSnapshot): boolean {
101
+ return this.once(name, this.properties(exposure), exposure.run, exposure.exposureId);
102
+ }
103
+ private properties(request: AdRequestSnapshot | AdExposureSnapshot): Record<string, unknown> {
104
+ const exposure = 'exposureId' in request ? request : null;
105
+ return { placement: request.placement, business_placement: request.placement,
106
+ ad_request_id: exposure?.exposureId ?? request.requestId, originating_request_id: request.requestId,
107
+ ad_format: request.format, provider: 'levelplay', is_test: request.isTest,
108
+ is_mock: request.isMock === true, is_demo: request.isDemo === true,
109
+ ...(request.adConfigRevision ? { ad_config_revision: request.adConfigRevision } : {}),
110
+ ...(exposure ? { network: exposure.network, ad_unit_id: exposure.adUnitId, ad_shown_at: exposure.shownAt,
111
+ ...(exposure.country ? { country: exposure.country, country_source: 'levelplay_sdk' } : {}) } : {}),
112
+ };
113
+ }
114
+ private once(name: string, properties: Record<string, unknown>, run: unknown, identity: string, occurredAt?: string): boolean {
115
+ return this.telemetry.track(name, properties, {
116
+ run, occurredAt, deduplicationKey: `levelplay:${name}:${identity}`,
117
+ });
118
+ }
119
+ }
@@ -0,0 +1,46 @@
1
+ import { _decorator, Component, sys, game, Game } from 'cc';
2
+ import { init, type CocosPlayScope } from '@virjay/playscope-cocos';
3
+ const { ccclass } = _decorator;
4
+
5
+ /** Mount one instance on the game's persistent owner node. */
6
+ @ccclass('PlayScopeExample')
7
+ export class PlayScopeExample extends Component {
8
+ private client: CocosPlayScope | null = null;
9
+ private allowed = false;
10
+ get telemetry(): CocosPlayScope | null { return this.client; }
11
+
12
+ onLoad(): void {
13
+ if (!this.client) this.setCollectionAllowed(false);
14
+ }
15
+ /** Connect this method to the game's actual stored consent / consent change callback. */
16
+ setCollectionAllowed(allowed: boolean): void {
17
+ if (this.client && this.allowed === allowed) return;
18
+ const previous = this.client;
19
+ this.client = null;
20
+ previous?.dispose();
21
+ this.client = init({
22
+ baseUrl: 'https://analytics.example.com',
23
+ projectId: 'YOUR_TEST_PROJECT_UUID',
24
+ projectKey: 'YOUR_PROJECT_CLIENT_KEY',
25
+ collectionEnabled: allowed,
26
+ client: { appVersion: '1.0.0', buildNumber: '1',
27
+ platform: sys.isNative ? sys.platform === sys.Platform.ANDROID ? 'android' : 'ios' : 'web',
28
+ locale: sys.language, timezoneOffsetMinutes: -new Date().getTimezoneOffset(), environment: 'test' },
29
+ }, {
30
+ storage: sys.localStorage,
31
+ lifecycle: {
32
+ onHide: callback => { game.on(Game.EVENT_HIDE, callback); return () => game.off(Game.EVENT_HIDE, callback); },
33
+ onShow: callback => { game.on(Game.EVENT_SHOW, callback); return () => game.off(Game.EVENT_SHOW, callback); },
34
+ },
35
+ });
36
+ this.allowed = allowed;
37
+ }
38
+ /** Call from the real launch path once; not on every consent change. */
39
+ recordLaunch(): boolean { return this.client?.track('app_launch', { reason: 'launch' }) ?? false; }
40
+ update(dt: number): void { this.client?.update(dt); }
41
+ onDestroy(): void {
42
+ const previous = this.client;
43
+ this.client = null;
44
+ previous?.dispose();
45
+ }
46
+ }
@@ -0,0 +1,129 @@
1
+ import { CocosPlayScope, telemetryUuid } from '@virjay/playscope-cocos';
2
+ import type { CocosRuntime, ClientContext, AdRevenueFact, ConfigurationContext, FlushResult } from '@virjay/playscope-cocos';
3
+ import { forwardLevelPlayRevenue } from '@virjay/playscope-cocos/adapters/levelplay';
4
+ import type { LevelPlayExposure, LevelPlayImpression } from '@virjay/playscope-cocos/adapters/levelplay';
5
+
6
+ export interface QuestConnection {
7
+ baseUrl: string;
8
+ projectId: string;
9
+ projectKey: string;
10
+ environment: 'production' | 'test';
11
+ }
12
+ interface Rules { schemaVersion: 1; revision: string; harvestTarget: number; season: string }
13
+ interface Assignment { assignmentId: string; experimentId: string | null; variantId: string | null }
14
+ interface Cache { config: Rules; assignment: Assignment }
15
+ interface Attribution { configRevision: string; source: 'bundled' | 'cache' | 'remote';
16
+ assignmentId: string | null; experimentId: string | null; variantId: string | null }
17
+ export interface QuestRun { runId: string; mode: 'garden-quest'; level: string; config: Attribution }
18
+ export interface QuestExposure extends LevelPlayExposure { run: QuestRun | null }
19
+ const BUNDLED: Rules = { schemaVersion: 1, revision: 'garden-bundled-1', harvestTarget: 3, season: 'spring' };
20
+ function readCache(value: unknown): Cache | null {
21
+ const saved = value as Cache | null, rules = saved?.config, assignment = saved?.assignment;
22
+ if (!rules || rules.schemaVersion !== 1 || typeof rules.revision !== 'string' || !rules.revision ||
23
+ !Number.isSafeInteger(rules.harvestTarget) || rules.harvestTarget < 1 || rules.harvestTarget > 20 ||
24
+ typeof rules.season !== 'string' || !rules.season || !assignment || typeof assignment.assignmentId !== 'string' || !assignment.assignmentId ||
25
+ !(assignment.experimentId === null && assignment.variantId === null ||
26
+ typeof assignment.experimentId === 'string' && !!assignment.experimentId && typeof assignment.variantId === 'string' && !!assignment.variantId)) return null;
27
+ return { config: { schemaVersion: 1, revision: rules.revision, harvestTarget: rules.harvestTarget, season: rules.season },
28
+ assignment: { assignmentId: assignment.assignmentId, experimentId: assignment.experimentId, variantId: assignment.variantId } };
29
+ }
30
+ /** Example protocol owned by this game. Custom events must be registered in its own game project. */
31
+ export function encodeQuestIncome(fact: Readonly<AdRevenueFact>) {
32
+ return { name: 'quest_ad_income', properties: { placement: fact.placement, exposure_id: fact.exposureId,
33
+ usd: fact.amount, provider: fact.provider, network: fact.network, ad_unit: fact.adUnitId, precision: fact.precision } };
34
+ }
35
+
36
+ /** Small independent host: gameplay and config application stay here; SDK only collects and delivers facts. */
37
+ export class QuestGarden {
38
+ private readonly sdk: CocosPlayScope;
39
+ private readonly client: ClientContext;
40
+ private rules: Rules = { ...BUNDLED };
41
+ private attribution: Attribution = { configRevision: BUNDLED.revision, source: 'bundled',
42
+ assignmentId: null, experimentId: null, variantId: null };
43
+ private active: { run: QuestRun; rules: Rules; harvests: number; activeMs: number } | null = null;
44
+ private started = false;
45
+ private disposed = false;
46
+ private completed = 0;
47
+ constructor(connection: QuestConnection, runtime: CocosRuntime, collectionEnabled: boolean,
48
+ client: Omit<ClientContext, 'environment'>) {
49
+ const { environment, ...endpoint } = connection;
50
+ this.client = JSON.parse(JSON.stringify({ ...client, environment }));
51
+ this.sdk = new CocosPlayScope({ ...endpoint, client: this.client, collectionEnabled,
52
+ // Separate project IDs are also required for real test/production projects.
53
+ storageKey: `quest-garden:${environment}:${connection.projectId}`, persistence: 'segmented',
54
+ queuePolicy: { maxEvents: 64, maxBytes: 512 * 1024, eventLimits: { quest_harvested: 48 } },
55
+ adRevenueEncoder: encodeQuestIncome,
56
+ restoreCache: saved => {
57
+ const cache = readCache(saved);
58
+ if (cache) this.apply(cache, 'cache');
59
+ return cache;
60
+ },
61
+ resolveConfiguration: context => this.resolveConfig(context),
62
+ }, runtime);
63
+ }
64
+ start(): void {
65
+ if (this.disposed || this.started) return;
66
+ this.started = true; this.sdk.start(); this.sdk.track('app_launch', { reason: 'launch' });
67
+ }
68
+ beginQuest(questId: string): string | null {
69
+ if (this.disposed || this.active || !questId) return null;
70
+ const run: QuestRun = { runId: telemetryUuid(), mode: 'garden-quest', level: questId, config: { ...this.attribution } };
71
+ this.active = { run, rules: { ...this.rules }, harvests: 0, activeMs: 0 };
72
+ this.sdk.track('game_start', {}, { run });
73
+ this.sdk.track('quest_started', { quest_id: questId, season: this.rules.season }, { run });
74
+ return run.runId;
75
+ }
76
+ harvest(): boolean {
77
+ if (this.disposed || !this.active) return false;
78
+ const quest = this.active;
79
+ quest.harvests++;
80
+ this.sdk.track('quest_harvested', { quest_id: quest.run.level, index: quest.harvests }, { run: quest.run });
81
+ if (quest.harvests >= quest.rules.harvestTarget) {
82
+ this.completed++;
83
+ this.sdk.track('quest_completed', { quest_id: quest.run.level, harvests: quest.harvests }, { run: quest.run });
84
+ this.sdk.track('run_ended', { outcome: 'completed', summary: { score: quest.harvests, durationMs: quest.activeMs,
85
+ metrics: { harvests: quest.harvests } } }, { run: quest.run });
86
+ this.active = null;
87
+ }
88
+ return true;
89
+ }
90
+ captureExposure(exposureId: string, placement: string, format: 'banner' | 'rewarded', isTest: boolean): QuestExposure {
91
+ return { exposureId, placement, businessPlacement: placement, format, isTest,
92
+ run: this.active ? JSON.parse(JSON.stringify(this.active.run)) : null };
93
+ }
94
+ recordIncome(impression: LevelPlayImpression, exposure: QuestExposure): boolean {
95
+ if (this.disposed) return false;
96
+ const { run, ...fact } = exposure;
97
+ return forwardLevelPlayRevenue(this.sdk, impression, fact, { run });
98
+ }
99
+ update(dt: number, playing = true): void {
100
+ if (this.disposed) return;
101
+ if (playing && this.active && Number.isFinite(dt) && dt > 0 && dt <= 1) this.active.activeMs += dt * 1000;
102
+ this.sdk.update(dt);
103
+ }
104
+ get progress() { return { completed: this.completed, questId: this.active?.run.level ?? null,
105
+ harvests: this.active?.harvests ?? 0, target: this.active?.rules.harvestTarget ?? this.rules.harvestTarget }; }
106
+ get installId() { return this.sdk.installId; }
107
+ get status() { return this.sdk.status; }
108
+ get diagnostics() { return this.sdk.getDiagnostics(); }
109
+ flush(): Promise<FlushResult> { return this.sdk.flushWithResult(); }
110
+ refreshConfiguration() { return this.sdk.refreshConfiguration(); }
111
+ dispose(): void { if (!this.disposed) { this.disposed = true; this.sdk.dispose(); } }
112
+ private apply(cache: Cache, source: 'cache' | 'remote'): void {
113
+ this.rules = cache.config;
114
+ this.attribution = { configRevision: cache.config.revision, source, ...cache.assignment };
115
+ }
116
+ private async resolveConfig(context: ConfigurationContext): Promise<number> {
117
+ const previous = readCache(context.cache);
118
+ const response = await context.request('/config/resolve', { apiVersion: 1, installId: this.installId,
119
+ client: this.client, supportedSchemas: [1], current: previous ? { revision: previous.config.revision, assignmentId: previous.assignment.assignmentId } : null }) as {
120
+ apiVersion: number; delivery: string; config: unknown; configRevision: string; assignment: unknown; refreshAfterSeconds: number };
121
+ if (this.disposed) return 3600;
122
+ const cache = readCache({ config: response?.delivery === 'full' ? response.config :
123
+ response?.delivery === 'not_modified' ? previous?.config : null, assignment: response?.assignment });
124
+ if (response?.apiVersion !== 1 || !cache || cache.config.revision !== response.configRevision) throw new Error('invalid_quest_config');
125
+ context.saveCache(cache); this.apply(cache, 'remote');
126
+ this.sdk.track('config_received', { revision: cache.config.revision });
127
+ return response.refreshAfterSeconds;
128
+ }
129
+ }
@@ -0,0 +1,23 @@
1
+ # 接入示例代码索引
2
+
3
+ 本目录提供了若干个开箱即用的参考实现,涵盖 Cocos Creator 组件、纯 TypeScript 核心集成、广告上报封装及端到端游戏对局示例。
4
+
5
+ ---
6
+
7
+ ## 示例文件一览
8
+
9
+ | 文件 | 适用场景 | 核心内容与职责 |
10
+ | :--- | :--- | :--- |
11
+ | **`PlayScopeExample.ts`** | **Cocos Creator 组件接入** | 可直接挂载在 Cocos 场景常驻节点的组件示例,包含单例管理、隐私授权切换、前后台监听、帧循环 `update` 及销毁处理。 |
12
+ | **`LevelPlayTelemetry.ts`** | **LevelPlay 广告全链路上报** | 规范化广告展示、曝光、点击、奖励及收入上报的适配层,演示如何生成独立 `exposureId`、配对展示与收入。 |
13
+ | **`CoreIntegration.ts`** | **纯 TypeScript / 已有框架项目** | 适用于已有独立调度管理器或非 Cocos 项目,演示如何直接创建和操作 `PlayScopeClient` 核心实例。 |
14
+ | **`QuestGarden.ts`** | **完整对局与配置归因参考** | 完整的端到端游戏宿主示例,演示关卡对局 `run` 对象的创建、局内配置冻结、云端配置拉取与缓存恢复。 |
15
+ | **`project-fixture.json`** | **后台项目配置参考** | 配套 `QuestGarden` 的项目创建材料、事件 Schema、云端配置及发布权重示例。 |
16
+
17
+ ---
18
+
19
+ ## 快速导航建议
20
+
21
+ - **如果你是在 Cocos Creator 中接入通用游戏**:优先参考 [PlayScopeExample.ts](PlayScopeExample.ts) 与 [LevelPlayTelemetry.ts](LevelPlayTelemetry.ts)。
22
+ - **如果你的工程已有现成的全局调度器**:参考 [CoreIntegration.ts](CoreIntegration.ts),将 SDK 核心注入已有调度,避免重复创建包装层。
23
+ - **如果你需要接入 A/B 测试或对局归因**:参考 [QuestGarden.ts](QuestGarden.ts) 中的 `run` 冻结与配置缓存应用逻辑。