@skillprint/cocos-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@skillprint/cocos-sdk",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Skillprint SDK for Cocos Creator 3.x",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc"
13
+ },
14
+ "peerDependencies": {
15
+ "cc": "*"
16
+ }
17
+ }
Binary file
@@ -0,0 +1,257 @@
1
+ import { Camera, RenderTexture, view, sys, native, director, Director } from 'cc';
2
+
3
+ export class ScreenshotUtility {
4
+ private logger: ((msg: string, level: 'Info' | 'Warning' | 'Error') => void) | null = null;
5
+
6
+ constructor(logger?: (msg: string, level: 'Info' | 'Warning' | 'Error') => void) {
7
+ if (logger) {
8
+ this.logger = logger;
9
+ }
10
+ }
11
+
12
+ private log(msg: string, level: 'Info' | 'Warning' | 'Error' = 'Info') {
13
+ if (this.logger) {
14
+ this.logger(msg, level);
15
+ } else {
16
+ const prefix = `[ScreenshotUtility] [${level}]`;
17
+ if (level === 'Error') {
18
+ console.error(`${prefix} ${msg}`);
19
+ } else if (level === 'Warning') {
20
+ console.warn(`${prefix} ${msg}`);
21
+ } else {
22
+ console.log(`${prefix} ${msg}`);
23
+ }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Captures a screenshot from the specified camera, flips it vertically,
29
+ * downscales it if necessary, and returns a JPEG Blob (Web) or ArrayBuffer (Native).
30
+ */
31
+ public async captureScreenshot(
32
+ camera: Camera,
33
+ maxWidth: number = 960,
34
+ jpegQuality: number = 60
35
+ ): Promise<Blob | ArrayBuffer | null> {
36
+ if (!camera) {
37
+ this.log('Cannot capture: Camera is null', 'Error');
38
+ return null;
39
+ }
40
+
41
+ try {
42
+ const visibleSize = view.getVisibleSize();
43
+ const width = Math.floor(visibleSize.width);
44
+ const height = Math.floor(visibleSize.height);
45
+
46
+ this.log(`Capturing screenshot: resolution ${width}x${height}`, 'Info');
47
+
48
+ // 1. Create RenderTexture
49
+ const renderTexture = new RenderTexture();
50
+ renderTexture.reset({
51
+ width: width,
52
+ height: height
53
+ });
54
+
55
+ // 2. Render camera into the texture by waiting for the render pipeline cycle
56
+ const prevTarget = camera.targetTexture;
57
+ camera.targetTexture = renderTexture;
58
+
59
+ await new Promise<void>((resolve) => {
60
+ director.once(Director.EVENT_AFTER_RENDER, resolve);
61
+ });
62
+
63
+ camera.targetTexture = prevTarget; // restore camera settings
64
+
65
+ // 3. Read pixels from GPU
66
+ const pixelBuffer = new Uint8Array(width * height * 4);
67
+ renderTexture.readPixels(0, 0, width, height, pixelBuffer);
68
+
69
+ // Clean up RenderTexture immediately to free GPU memory
70
+ renderTexture.destroy();
71
+
72
+ // 4. Flip pixel coordinates vertically (WebGL reads bottom-to-top)
73
+ this.flipY(pixelBuffer, width, height);
74
+
75
+ // 5. Handle Platform-Specific Encoding
76
+ if (sys.isBrowser) {
77
+ return await this.encodeOnWeb(pixelBuffer, width, height, maxWidth, jpegQuality);
78
+ } else if (sys.isNative && !!native && typeof native.saveImageData === 'function') {
79
+ return await this.encodeOnNative(pixelBuffer, width, height, maxWidth, jpegQuality);
80
+ } else {
81
+ this.log('Platform not supported for screenshot encoding. Returning raw buffer.', 'Warning');
82
+ return pixelBuffer.buffer;
83
+ }
84
+ } catch (err: any) {
85
+ this.log(`Failed to capture screenshot: ${err.message || err}`, 'Error');
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Vertically flips the raw pixel buffer in-place.
92
+ */
93
+ private flipY(pixels: Uint8Array, width: number, height: number) {
94
+ const rowBytes = width * 4;
95
+ const tempRow = new Uint8Array(rowBytes);
96
+ for (let row = 0; row < Math.floor(height / 2); row++) {
97
+ const topOffset = row * rowBytes;
98
+ const bottomOffset = (height - 1 - row) * rowBytes;
99
+ // Swap top and bottom rows
100
+ tempRow.set(pixels.subarray(topOffset, topOffset + rowBytes));
101
+ pixels.set(pixels.subarray(bottomOffset, bottomOffset + rowBytes), topOffset);
102
+ pixels.set(tempRow, bottomOffset);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * HTML5 Canvas based JPEG encoder and scaling for web platforms
108
+ */
109
+ private encodeOnWeb(
110
+ pixels: Uint8Array,
111
+ width: number,
112
+ height: number,
113
+ maxWidth: number,
114
+ jpegQuality: number
115
+ ): Promise<Blob | null> {
116
+ return new Promise((resolve) => {
117
+ try {
118
+ // Determine target dimensions
119
+ let destWidth = width;
120
+ let destHeight = height;
121
+ if (maxWidth > 0 && width > maxWidth) {
122
+ const scale = maxWidth / width;
123
+ destWidth = maxWidth;
124
+ destHeight = Math.floor(height * scale);
125
+ }
126
+
127
+ // Create offscreen canvas for rendering
128
+ const canvas = document.createElement('canvas');
129
+ canvas.width = destWidth;
130
+ canvas.height = destHeight;
131
+ const ctx = canvas.getContext('2d');
132
+
133
+ if (!ctx) {
134
+ this.log('Could not get HTML5 canvas 2D context', 'Error');
135
+ resolve(null);
136
+ return;
137
+ }
138
+
139
+ if (destWidth === width && destHeight === height) {
140
+ // Direct copy
141
+ const imgData = ctx.createImageData(width, height);
142
+ imgData.data.set(pixels);
143
+ ctx.putImageData(imgData, 0, 0);
144
+ } else {
145
+ // Downscale using intermediate canvas drawing
146
+ const srcCanvas = document.createElement('canvas');
147
+ srcCanvas.width = width;
148
+ srcCanvas.height = height;
149
+ const srcCtx = srcCanvas.getContext('2d');
150
+ if (srcCtx) {
151
+ const imgData = srcCtx.createImageData(width, height);
152
+ imgData.data.set(pixels);
153
+ srcCtx.putImageData(imgData, 0, 0);
154
+ // Render downscaled image
155
+ ctx.drawImage(srcCanvas, 0, 0, destWidth, destHeight);
156
+ } else {
157
+ resolve(null);
158
+ return;
159
+ }
160
+ }
161
+
162
+ const qualityFactor = Math.max(0.01, Math.min(1.0, jpegQuality / 100));
163
+ canvas.toBlob((blob) => {
164
+ resolve(blob);
165
+ }, 'image/jpeg', qualityFactor);
166
+ } catch (err: any) {
167
+ this.log(`Web encoding failed: ${err.message || err}`, 'Error');
168
+ resolve(null);
169
+ }
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Native file and image API based JPEG encoder for Android/iOS/Desktop
175
+ */
176
+ private async encodeOnNative(
177
+ pixels: Uint8Array,
178
+ width: number,
179
+ height: number,
180
+ maxWidth: number,
181
+ jpegQuality: number
182
+ ): Promise<ArrayBuffer | null> {
183
+ try {
184
+ // Note: Cocos Creator Native saveImageData saves raw pixel buffers to file.
185
+ // Under the hood, if the filename has a .jpg extension, it handles JPEG compression.
186
+ const writablePath = native.fileUtils.getWritablePath();
187
+ const filePath = `${writablePath}temp_screenshot.jpg`;
188
+
189
+ // If downscaling is needed natively, we pass smaller dimensions if native API supports scaling,
190
+ // otherwise saveImageData handles saving raw buffer.
191
+ let targetWidth = width;
192
+ let targetHeight = height;
193
+ let finalPixels = pixels;
194
+
195
+ if (maxWidth > 0 && width > maxWidth) {
196
+ const scale = maxWidth / width;
197
+ targetWidth = maxWidth;
198
+ targetHeight = Math.floor(height * scale);
199
+ // Perform a simple downsampling if size is reduced (e.g. skip pixels)
200
+ // In Cocos 3.x, saveImageData is standard, so we can save it and let the engine compress it.
201
+ // We'll downsample finalPixels array to match target size
202
+ finalPixels = this.downsamplePixels(pixels, width, height, targetWidth, targetHeight);
203
+ }
204
+
205
+ // Save image using Cocos native APIs (saves as JPEG on .jpg extension)
206
+ // Note: native.saveImageData returns a Promise or runs synchronously. In 3.8.x, it returns a Promise.
207
+ await native.saveImageData(finalPixels, targetWidth, targetHeight, filePath);
208
+
209
+ // Read the saved file back into memory
210
+ const arrayBuffer = native.fileUtils.getDataFromFile(filePath);
211
+
212
+ // Clean up the temporary file
213
+ native.fileUtils.removeFile(filePath);
214
+
215
+ if (arrayBuffer && arrayBuffer.byteLength > 0) {
216
+ this.log(`Native screenshot encoded successfully. Size: ${arrayBuffer.byteLength} bytes`, 'Info');
217
+ return arrayBuffer;
218
+ } else {
219
+ this.log('Failed to read native saved screenshot back from local storage', 'Error');
220
+ return null;
221
+ }
222
+ } catch (err: any) {
223
+ this.log(`Native encoding failed: ${err.message || err}`, 'Error');
224
+ return null;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Simple pixel downsampling helper for native builds without canvas API
230
+ */
231
+ private downsamplePixels(
232
+ pixels: Uint8Array,
233
+ srcW: number,
234
+ srcH: number,
235
+ destW: number,
236
+ destH: number
237
+ ): Uint8Array {
238
+ const destPixels = new Uint8Array(destW * destH * 4);
239
+ const xRatio = srcW / destW;
240
+ const yRatio = srcH / destH;
241
+
242
+ for (let y = 0; y < destH; y++) {
243
+ for (let x = 0; x < destW; x++) {
244
+ const srcX = Math.floor(x * xRatio);
245
+ const srcY = Math.floor(y * yRatio);
246
+ const srcIdx = (srcY * srcW + srcX) * 4;
247
+ const destIdx = (y * destW + x) * 4;
248
+
249
+ destPixels[destIdx] = pixels[srcIdx]; // R
250
+ destPixels[destIdx + 1] = pixels[srcIdx + 1]; // G
251
+ destPixels[destIdx + 2] = pixels[srcIdx + 2]; // B
252
+ destPixels[destIdx + 3] = pixels[srcIdx + 3]; // A
253
+ }
254
+ }
255
+ return destPixels;
256
+ }
257
+ }
@@ -0,0 +1,323 @@
1
+ import { ParameterInfo, ParameterUpdateResult, PollResultsResponse } from './SkillprintTypes';
2
+
3
+ export class SkillprintAPIClient {
4
+ private baseUrl: string;
5
+ private partnerApiKey: string;
6
+ private userToken: string | null = null;
7
+ private logger: ((msg: string, level: 'Info' | 'Warning' | 'Error') => void) | null = null;
8
+
9
+ // Endpoints
10
+ private static readonly START_SESSION_ENDPOINT = '/games/api/sessions/';
11
+ private static readonly UPLOAD_SCREENSHOTS_ENDPOINT = '/games/api/record-session/{sessionId}/';
12
+ private static readonly POLL_RESULTS_ENDPOINT = '/games/api/sessions/{sessionId}/';
13
+ private static readonly CREATE_USER_ENDPOINT = '/partners/api/users/add/';
14
+ private static readonly GET_USER_TOKEN_ENDPOINT = '/partners/api/users/auth/token/';
15
+
16
+ // Retry settings
17
+ private static readonly MAX_UPLOAD_RETRIES = 2;
18
+ private static readonly RETRY_BASE_DELAY_MS = 1000;
19
+
20
+ constructor(
21
+ baseUrl: string,
22
+ partnerApiKey: string,
23
+ logger?: (msg: string, level: 'Info' | 'Warning' | 'Error') => void
24
+ ) {
25
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
26
+ this.partnerApiKey = partnerApiKey;
27
+ if (logger) {
28
+ this.logger = logger;
29
+ }
30
+ }
31
+
32
+ private log(msg: string, level: 'Info' | 'Warning' | 'Error' = 'Info') {
33
+ if (this.logger) {
34
+ this.logger(msg, level);
35
+ } else {
36
+ const prefix = `[SkillprintAPIClient] [${level}]`;
37
+ if (level === 'Error') {
38
+ console.error(`${prefix} ${msg}`);
39
+ } else if (level === 'Warning') {
40
+ console.warn(`${prefix} ${msg}`);
41
+ } else {
42
+ console.log(`${prefix} ${msg}`);
43
+ }
44
+ }
45
+ }
46
+
47
+ public async startSession(
48
+ sessionId: string,
49
+ targetMood: string,
50
+ customPlayerId: string | null,
51
+ gameName: string,
52
+ gameParameters: ParameterInfo[]
53
+ ): Promise<boolean> {
54
+ const url = `${this.baseUrl}${SkillprintAPIClient.START_SESSION_ENDPOINT}`;
55
+ this.log(`Starting session: POST ${url}`, 'Info');
56
+ this.log(`Starting session: MOOD ${targetMood}`, 'Warning');
57
+
58
+ // Provision/retrieve user token if customPlayerId is provided
59
+ if (customPlayerId) {
60
+ try {
61
+ this.userToken = await this.createOrGetUserToken(customPlayerId);
62
+ if (this.userToken) {
63
+ this.log(`User token obtained successfully for player: ${customPlayerId}`, 'Info');
64
+ } else {
65
+ this.log(`Could not retrieve user token for player: ${customPlayerId}. Continuing without token.`, 'Warning');
66
+ }
67
+ } catch (err: any) {
68
+ this.log(`Error obtaining user token: ${err.message || err}. Continuing without token.`, 'Warning');
69
+ }
70
+ }
71
+
72
+ const requestData = {
73
+ sessionId: sessionId,
74
+ game: gameName,
75
+ targetMood: targetMood,
76
+ gameParameters: gameParameters
77
+ };
78
+
79
+ const headers: HeadersInit = {
80
+ 'Content-Type': 'application/json',
81
+ 'Authorization': `Api-Key ${this.partnerApiKey}`
82
+ };
83
+
84
+ if (this.userToken) {
85
+ headers['X-Auth-Token'] = `Token ${this.userToken}`;
86
+ }
87
+
88
+ try {
89
+ const response = await fetch(url, {
90
+ method: 'POST',
91
+ headers: headers,
92
+ body: JSON.stringify(requestData)
93
+ });
94
+
95
+ const text = await response.text();
96
+ if (response.ok) {
97
+ this.log(`StartSession successful. Response: ${text}`, 'Info');
98
+ return true;
99
+ } else {
100
+ this.log(`StartSession Error: ${response.statusText}. Response: ${text}`, 'Error');
101
+ return false;
102
+ }
103
+ } catch (err: any) {
104
+ this.log(`StartSession Network Error: ${err.message || err}`, 'Error');
105
+ return false;
106
+ }
107
+ }
108
+
109
+ public async postScreenshots(
110
+ sessionId: string,
111
+ screenshots: (Blob | ArrayBuffer)[],
112
+ isLastChunk: boolean
113
+ ): Promise<boolean> {
114
+ const endpoint = SkillprintAPIClient.UPLOAD_SCREENSHOTS_ENDPOINT.replace('{sessionId}', sessionId);
115
+ const url = `${this.baseUrl}${endpoint}`;
116
+ this.log(`Posting ${screenshots.length} screenshots (isLastChunk: ${isLastChunk}): POST ${url}`, 'Info');
117
+
118
+ if (screenshots.length === 0 && !isLastChunk) {
119
+ this.log(`No screenshots provided, and isLastChunk is false. Skipping.`, 'Warning');
120
+ return false;
121
+ }
122
+
123
+ let attempt = 0;
124
+ let succeeded = false;
125
+ let lastError = '';
126
+
127
+ while (attempt <= SkillprintAPIClient.MAX_UPLOAD_RETRIES && !succeeded) {
128
+ if (attempt > 0) {
129
+ const delay = SkillprintAPIClient.RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
130
+ this.log(`Retrying screenshot upload (attempt ${attempt + 1}/${SkillprintAPIClient.MAX_UPLOAD_RETRIES + 1}) after ${delay}ms...`, 'Warning');
131
+ await new Promise((resolve) => setTimeout(resolve, delay));
132
+ }
133
+
134
+ try {
135
+ const formData = new FormData();
136
+ formData.append('is_last_chunk', isLastChunk.toString().toLowerCase());
137
+
138
+ screenshots.forEach((screenshot, i) => {
139
+ const blob = screenshot instanceof ArrayBuffer ? new Blob([screenshot], { type: 'image/jpeg' }) : screenshot;
140
+ formData.append(`screenshot${i}`, blob, `screenshot_${i}.jpg`);
141
+ });
142
+
143
+ const headers: HeadersInit = {
144
+ 'Authorization': `Api-Key ${this.partnerApiKey}`
145
+ };
146
+ if (this.userToken) {
147
+ headers['X-Auth-Token'] = `Token ${this.userToken}`;
148
+ }
149
+
150
+ // Note: Do not set Content-Type header when sending FormData.
151
+ // The browser will automatically set the correct multipart/form-data boundary.
152
+ const response = await fetch(url, {
153
+ method: 'POST',
154
+ headers: headers,
155
+ body: formData
156
+ });
157
+
158
+ const text = await response.text();
159
+ if (response.ok) {
160
+ this.log(`PostScreenshots successful. Response: ${text}`, 'Info');
161
+ succeeded = true;
162
+ } else {
163
+ lastError = `Status: ${response.statusText} | Response: ${text}`;
164
+ this.log(`PostScreenshots Error (attempt ${attempt + 1}): ${lastError}`, 'Error');
165
+ }
166
+ } catch (err: any) {
167
+ lastError = err.message || String(err);
168
+ this.log(`PostScreenshots Network Error (attempt ${attempt + 1}): ${lastError}`, 'Error');
169
+ }
170
+
171
+ attempt++;
172
+ }
173
+
174
+ if (!succeeded) {
175
+ this.log(`PostScreenshots failed after ${SkillprintAPIClient.MAX_UPLOAD_RETRIES + 1} attempts. Last error: ${lastError}`, 'Error');
176
+ }
177
+
178
+ return succeeded;
179
+ }
180
+
181
+ public async pollParameterResults(sessionId: string): Promise<ParameterUpdateResult[]> {
182
+ const endpoint = SkillprintAPIClient.POLL_RESULTS_ENDPOINT.replace('{sessionId}', sessionId);
183
+ const url = `${this.baseUrl}${endpoint}`;
184
+ this.log(`Polling results: GET ${url}`, 'Info');
185
+
186
+ const headers: HeadersInit = {
187
+ 'Authorization': `Api-Key ${this.partnerApiKey}`,
188
+ 'Accept': 'application/json'
189
+ };
190
+ if (this.userToken) {
191
+ headers['X-Auth-Token'] = `Token ${this.userToken}`;
192
+ }
193
+
194
+ try {
195
+ const response = await fetch(url, {
196
+ method: 'GET',
197
+ headers: headers
198
+ });
199
+
200
+ const text = await response.text();
201
+ if (response.ok) {
202
+ this.log(`PollResults successful. Response: ${text}`, 'Info');
203
+ let data: any;
204
+ try {
205
+ data = JSON.parse(text);
206
+ } catch (jsonErr: any) {
207
+ // Try parsing as array directly if it was wrapped or malformed
208
+ if (text.trim().startsWith('[')) {
209
+ data = { parameterUpdates: JSON.parse(text) };
210
+ } else {
211
+ throw jsonErr;
212
+ }
213
+ }
214
+
215
+ let updates: ParameterUpdateResult[] = [];
216
+ if (data && Array.isArray(data.parameterUpdates)) {
217
+ updates = data.parameterUpdates;
218
+ } else if (Array.isArray(data)) {
219
+ updates = data;
220
+ }
221
+
222
+ // Map updates to extract the parsed values
223
+ updates.forEach((update) => {
224
+ this.log(`Parameter Update: ${update.parameterName} = ${this.getParsedValue(update)}`, 'Info');
225
+ });
226
+
227
+ return updates;
228
+ } else {
229
+ this.log(`PollResults Error: ${response.statusText}. Response: ${text}`, 'Error');
230
+ return [];
231
+ }
232
+ } catch (err: any) {
233
+ this.log(`PollResults Error: ${err.message || err}`, 'Error');
234
+ return [];
235
+ }
236
+ }
237
+
238
+ public getParsedValue(update: ParameterUpdateResult): any {
239
+ // Retrieve the newValue or find the key with the parameterName (for legacy or alternative responses)
240
+ if (update.newValue !== undefined && update.newValue !== null) {
241
+ return update.newValue;
242
+ }
243
+ if (update.parameterName && update[update.parameterName] !== undefined) {
244
+ return update[update.parameterName];
245
+ }
246
+ return null;
247
+ }
248
+
249
+ public async createOrGetUserToken(customPlayerId: string): Promise<string | null> {
250
+ if (!customPlayerId) return null;
251
+
252
+ // Try getting token first
253
+ let token = await this.getUserToken(customPlayerId);
254
+ if (token) return token;
255
+
256
+ // User doesn't exist, create user
257
+ const created = await this.createUser(customPlayerId);
258
+ if (created) {
259
+ // Retrieve token again after creation
260
+ token = await this.getUserToken(customPlayerId);
261
+ return token;
262
+ }
263
+
264
+ return null;
265
+ }
266
+
267
+ private async createUser(internalId: string): Promise<boolean> {
268
+ const url = `${this.baseUrl}${SkillprintAPIClient.CREATE_USER_ENDPOINT}`;
269
+ this.log(`Creating user: POST ${url} with internalId: ${internalId}`, 'Info');
270
+
271
+ try {
272
+ const response = await fetch(url, {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Content-Type': 'application/json',
276
+ 'Authorization': `Api-Key ${this.partnerApiKey}`
277
+ },
278
+ body: JSON.stringify({ internalId: internalId })
279
+ });
280
+
281
+ const text = await response.text();
282
+ if (response.ok) {
283
+ this.log(`CreateUser successful. Response: ${text}`, 'Info');
284
+ return true;
285
+ } else {
286
+ this.log(`CreateUser Error: ${response.statusText}. Response: ${text}`, 'Error');
287
+ return false;
288
+ }
289
+ } catch (err: any) {
290
+ this.log(`CreateUser Network Error: ${err.message || err}`, 'Error');
291
+ return false;
292
+ }
293
+ }
294
+
295
+ private async getUserToken(internalId: string): Promise<string | null> {
296
+ const url = `${this.baseUrl}${SkillprintAPIClient.GET_USER_TOKEN_ENDPOINT}`;
297
+ this.log(`Getting user token: POST ${url} with internalId: ${internalId}`, 'Info');
298
+
299
+ try {
300
+ const response = await fetch(url, {
301
+ method: 'POST',
302
+ headers: {
303
+ 'Content-Type': 'application/json',
304
+ 'Authorization': `Api-Key ${this.partnerApiKey}`
305
+ },
306
+ body: JSON.stringify({ internalId: internalId })
307
+ });
308
+
309
+ const text = await response.text();
310
+ if (response.ok) {
311
+ this.log(`GetUserToken successful. Response: ${text}`, 'Info');
312
+ const data = JSON.parse(text);
313
+ return data.token || null;
314
+ } else {
315
+ this.log(`GetUserToken Error: ${response.statusText}. Response: ${text}`, 'Error');
316
+ return null;
317
+ }
318
+ } catch (err: any) {
319
+ this.log(`GetUserToken Network Error: ${err.message || err}`, 'Error');
320
+ return null;
321
+ }
322
+ }
323
+ }
@@ -0,0 +1,91 @@
1
+ import { _decorator, Component, Enum } from 'cc';
2
+ import { ApiEnvironment, SkillprintConfig } from './SkillprintTypes';
3
+
4
+ const { ccclass, property } = _decorator;
5
+
6
+ Enum(ApiEnvironment);
7
+
8
+ @ccclass('SkillprintConfigComponent')
9
+ export class SkillprintConfigComponent extends Component {
10
+ @property({
11
+ displayName: 'Game Name (Slug)',
12
+ tooltip: 'The unique slug registered for your game, e.g. fruit-boom'
13
+ })
14
+ public gameName = 'cocos-demo-game';
15
+
16
+ @property({
17
+ type: Enum(ApiEnvironment),
18
+ displayName: 'Target Environment'
19
+ })
20
+ public targetEnvironment = ApiEnvironment.Staging;
21
+
22
+ @property({
23
+ displayName: 'Staging API Key',
24
+ })
25
+ public stagingPartnerApiKey = '';
26
+
27
+ @property({
28
+ displayName: 'Staging API Base URL'
29
+ })
30
+ public stagingApiBaseUrl = 'https://api.staging.skillprint.co';
31
+
32
+ @property({
33
+ displayName: 'Production API Key',
34
+ })
35
+ public productionPartnerApiKey = '';
36
+
37
+ @property({
38
+ displayName: 'Production API Base URL'
39
+ })
40
+ public productionApiBaseUrl = 'https://api.skillprint.co';
41
+
42
+ @property({
43
+ displayName: 'Screenshot Max Width',
44
+ tooltip: 'Maximum width for screenshots. Images wider than this are downscaled. 0 = no limit.'
45
+ })
46
+ public screenshotMaxWidth = 960;
47
+
48
+ @property({
49
+ displayName: 'Screenshot JPEG Quality',
50
+ tooltip: 'Compression quality (1-100). Lower values are smaller files.'
51
+ })
52
+ public screenshotJpegQuality = 60;
53
+
54
+ @property({
55
+ displayName: 'Screenshot Interval (s)'
56
+ })
57
+ public screenshotIntervalSeconds = 2.0;
58
+
59
+ @property({
60
+ displayName: 'Screenshot Post Interval (s)'
61
+ })
62
+ public screenshotPostIntervalSeconds = 5.0;
63
+
64
+ @property({
65
+ displayName: 'Poll Results Interval (s)'
66
+ })
67
+ public pollResultsIntervalSeconds = 5.0;
68
+
69
+ @property({
70
+ displayName: 'Enable Debug Logging'
71
+ })
72
+ public enableDebugLogging = false;
73
+
74
+ public getConfig(): SkillprintConfig {
75
+ return {
76
+ gameName: this.gameName,
77
+ targetEnvironment: this.targetEnvironment,
78
+ productionPartnerApiKey: this.productionPartnerApiKey,
79
+ productionApiBaseUrl: this.productionApiBaseUrl,
80
+ stagingPartnerApiKey: this.stagingPartnerApiKey,
81
+ stagingApiBaseUrl: this.stagingApiBaseUrl,
82
+ screenshotMaxWidth: this.screenshotMaxWidth,
83
+ screenshotJpegQuality: this.screenshotJpegQuality,
84
+ screenshotIntervalSeconds: this.screenshotIntervalSeconds,
85
+ screenshotPostIntervalSeconds: this.screenshotPostIntervalSeconds,
86
+ pollResultsIntervalSeconds: this.pollResultsIntervalSeconds,
87
+ enableDebugLogging: this.enableDebugLogging,
88
+ gameParameters: []
89
+ };
90
+ }
91
+ }