From 09e80e505215a5cba4b56cff3b902f466f1fc770 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 6 Sep 2026 00:00:19 +0800 Subject: [PATCH] feat(ui): wire Logo native loading progress --- .../ui/prefabs/widgets/Layer1_Logo.prefab | 319 +++++++++++++++--- .../assets/scenes/LocalPlatformLogin.scene | 12 +- .../LocalPlatformLogin.ts | 34 +- .../local-platform-login/local-login-view.ts | 42 +++ .../local-platform-login/view.test.ts | 65 +++- .../2026-09-05-logo-progress-acceptance.md | 35 ++ 6 files changed, 421 insertions(+), 86 deletions(-) create mode 100644 docs/superpowers/reports/2026-09-05-logo-progress-acceptance.md diff --git a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab index 112df3f..6650630 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab +++ b/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab @@ -22,13 +22,13 @@ "__id__": 2 }, { - "__id__": 40 + "__id__": 50 } ], "_active": true, "_components": [], "_prefab": { - "__id__": 50 + "__id__": 60 }, "_lpos": { "__type__": "cc.Vec3", @@ -75,23 +75,23 @@ "__id__": 11 }, { - "__id__": 19 - }, - { - "__id__": 27 - } - ], - "_active": true, - "_components": [ - { - "__id__": 35 + "__id__": 29 }, { "__id__": 37 } ], + "_active": true, + "_components": [ + { + "__id__": 45 + }, + { + "__id__": 47 + } + ], "_prefab": { - "__id__": 39 + "__id__": 49 }, "_lpos": { "__type__": "cc.Vec3", @@ -305,21 +305,28 @@ "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 12 + } + ], "_active": true, "_components": [ { - "__id__": 12 + "__id__": 20 }, { - "__id__": 14 + "__id__": 22 }, { - "__id__": 16 + "__id__": 24 + }, + { + "__id__": 26 } ], "_prefab": { - "__id__": 18 + "__id__": 28 }, "_lpos": { "__type__": "cc.Vec3", @@ -350,6 +357,181 @@ }, "_id": "" }, + { + "__type__": "cc.Node", + "_name": "Fill", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 11 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 13 + }, + { + "__id__": 15 + }, + { + "__id__": 17 + } + ], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 14 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1600, + "height": 7 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "11wryNsjhNPqbXqeNtm4GL" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 16 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "04b97952-ff19-40c5-b2f0-d5a84ff93bb5@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 3, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "93ObiVr8BLmL02Gr+0Ygcn" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 12 + }, + "_enabled": true, + "__prefab": { + "__id__": 18 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 416, + "_originalHeight": 7, + "_alignMode": 1, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "64uW+20zNAZ7vzhVMp3KqI" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "09/GP9iWJEppj1bGMyhJQK", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -360,7 +542,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 13 + "__id__": 21 }, "_contentSize": { "__type__": "cc.Size", @@ -388,7 +570,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 15 + "__id__": 23 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -433,7 +615,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 17 + "__id__": 25 }, "_alignFlags": 21, "_target": null, @@ -459,6 +641,31 @@ "__type__": "cc.CompPrefabInfo", "fileId": "w8o18qHP5Wem3Gb4ztYDwQ" }, + { + "__type__": "cc.ProgressBar", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 27 + }, + "_barSprite": { + "__id__": 15 + }, + "_mode": 2, + "_totalLength": 1, + "_progress": 0, + "_reverse": false, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "a0JQfp8UpBJbgzk6ZEyuhB" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -484,17 +691,17 @@ "_active": true, "_components": [ { - "__id__": 20 + "__id__": 30 }, { - "__id__": 22 + "__id__": 32 }, { - "__id__": 24 + "__id__": 34 } ], "_prefab": { - "__id__": 26 + "__id__": 36 }, "_lpos": { "__type__": "cc.Vec3", @@ -531,11 +738,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 31 }, "_contentSize": { "__type__": "cc.Size", @@ -559,11 +766,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 23 + "__id__": 33 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -604,11 +811,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 19 + "__id__": 29 }, "_enabled": true, "__prefab": { - "__id__": 25 + "__id__": 35 }, "_alignFlags": 16, "_target": null, @@ -659,17 +866,17 @@ "_active": true, "_components": [ { - "__id__": 28 + "__id__": 38 }, { - "__id__": 30 + "__id__": 40 }, { - "__id__": 32 + "__id__": 42 } ], "_prefab": { - "__id__": 34 + "__id__": 44 }, "_lpos": { "__type__": "cc.Vec3", @@ -706,11 +913,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 29 + "__id__": 39 }, "_contentSize": { "__type__": "cc.Size", @@ -734,11 +941,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 31 + "__id__": 41 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -802,11 +1009,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 27 + "__id__": 37 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 43 }, "_alignFlags": 16, "_target": null, @@ -855,7 +1062,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 36 + "__id__": 46 }, "_contentSize": { "__type__": "cc.Size", @@ -883,7 +1090,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 38 + "__id__": 48 }, "_alignFlags": 0, "_target": null, @@ -932,13 +1139,13 @@ }, "_children": [ { - "__id__": 41 + "__id__": 51 } ], "_active": true, "_components": [], "_prefab": { - "__id__": 49 + "__id__": 59 }, "_lpos": { "__type__": "cc.Vec3", @@ -975,23 +1182,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 40 + "__id__": 50 }, "_children": [], "_active": true, "_components": [ { - "__id__": 42 + "__id__": 52 }, { - "__id__": 44 + "__id__": 54 }, { - "__id__": 46 + "__id__": 56 } ], "_prefab": { - "__id__": 48 + "__id__": 58 }, "_lpos": { "__type__": "cc.Vec3", @@ -1028,11 +1235,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 43 + "__id__": 53 }, "_contentSize": { "__type__": "cc.Size", @@ -1056,11 +1263,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 45 + "__id__": 55 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1101,11 +1308,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 41 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 57 }, "_alignFlags": 16, "_target": null, diff --git a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene index d6180a9..c591c27 100644 --- a/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene +++ b/cocoscreator_projects/YouleNexus/assets/scenes/LocalPlatformLogin.scene @@ -275,7 +275,7 @@ "_enabled": true, "__prefab": null, "loadingPrefab": { - "__uuid__": "4db0114a-f751-4931-bfaf-4fef31430583", + "__uuid__": "3e5a41f3-b043-45a5-94f6-e299ea9acc61", "__expectedType__": "cc.Prefab" }, "loginPrefab": { @@ -371,10 +371,10 @@ "__id__": 8 }, "asset": { - "__uuid__": "4db0114a-f751-4931-bfaf-4fef31430583", + "__uuid__": "3e5a41f3-b043-45a5-94f6-e299ea9acc61", "__expectedType__": "cc.Prefab" }, - "fileId": "RyJAROM7sGTy6Li8CWDPZA", + "fileId": "D6NzGhQBg/6lYKr/OZsmJQ", "instance": { "__id__": 10 }, @@ -383,7 +383,7 @@ }, { "__type__": "cc.PrefabInstance", - "fileId": "ff8m2dc1lLAqLAsOz8udcb", + "fileId": "00Yyr3cfRNcZX1WLG7VLxz", "prefabRootNode": null, "mountedChildren": [], "mountedComponents": [], @@ -411,12 +411,12 @@ "propertyPath": [ "_name" ], - "value": "Layer614_Loading" + "value": "Layer1_Logo" }, { "__type__": "cc.TargetInfo", "localID": [ - "RyJAROM7sGTy6Li8CWDPZA" + "D6NzGhQBg/6lYKr/OZsmJQ" ] }, { diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts index 718f156..fb47160 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts @@ -1,4 +1,4 @@ -import { _decorator, Component, Label, Node, Prefab, Sprite, UITransform, instantiate, isValid, sys } from 'cc'; +import { _decorator, assetManager, Component, Label, Node, Prefab, ProgressBar, Sprite, instantiate, isValid, sys } from 'cc'; import { DEBUG } from 'cc/env'; import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts'; import type { NativeHost } from '../../framework/config/sources/native-settings.ts'; @@ -6,7 +6,7 @@ import type { RuntimeConfig } from '../../framework/config/runtime-config.ts'; import type { PageModel } from '../../framework/presentation/ui-contracts.ts'; import { createLocalLoginHost, type LocalLoginHost } from './local-login-host.ts'; import type { LocalLoginEvidence } from './local-login-transport.ts'; -import { CocosLocalLoginView, createCocosFrameScheduler, requireNode } from './local-login-view.ts'; +import { CocosLocalLoginView, createAssetLoadTask, createCocosFrameScheduler, requireNode } from './local-login-view.ts'; const { ccclass, property } = _decorator; const MINIMUM_LOADING_MS = 500; @@ -26,7 +26,7 @@ export class LocalPlatformLogin extends Component { private host: LocalLoginHost | null = null; private view: CocosLocalLoginView | null = null; - private spinner: Node | null = null; + private resourceProgress: ProgressBar | null = null; private loginInstance: Node | null = null; private stopped = false; private firstDrawAt: number | null = null; @@ -50,11 +50,11 @@ export class LocalPlatformLogin extends Component { resultLabel: this.required(this.resultLabel, 'resultLabel') }); this.required(this.loadingPrefab, 'loadingPrefab'); this.required(this.loginPrefab, 'loginPrefab'); - this.spinner = requireNode(loadingRoot, 'Layer614_Loading/group-40/载入动画'); + const logo = requireNode(loadingRoot, 'Layer1_Logo'); + this.resourceProgress = requireNode(logo, 'group-1/Spirit254').getComponent(ProgressBar); + if (this.resourceProgress === null) throw new Error('Layer1_Logo/group-1/Spirit254 requires ProgressBar'); + for (const path of ['group-1/Spirit3103', 'group-1/Spirit3104', 'group-32']) requireNode(logo, path).active = false; this.validateSprites(loadingRoot); - if (this.spinner.getComponent(UITransform) === null || this.spinner.getComponent(Sprite) === null) { - throw new Error('Loading spinner requires UITransform and Sprite'); - } stopButton.on(Node.EventType.TOUCH_END, this.stopTouch); this.view.activate('loading'); const firstDraw = this.nextDraw().then(() => { @@ -118,11 +118,21 @@ export class LocalPlatformLogin extends Component { private async loadResources(): Promise { if (this.stopped) throw new Error('Local login resources cancelled'); - const prefab = this.required(this.loginPrefab, 'loginPrefab'); + const serializedPrefab = this.required(this.loginPrefab, 'loginPrefab'); const loading = this.required(this.loadingPrefab, 'loadingPrefab'); - if (!isValid(prefab) || !isValid(prefab.data) || !isValid(loading) || !isValid(loading.data)) { + if (!isValid(serializedPrefab) || !isValid(serializedPrefab.data) || !isValid(loading) || !isValid(loading.data)) { throw new Error('Local login serialized Prefab dependency is invalid'); } + const progress = this.required(this.resourceProgress, 'resource ProgressBar'); + const task = createAssetLoadTask((onProgress, onComplete) => { + assetManager.loadAny(serializedPrefab.uuid, onProgress, (error, asset) => onComplete(error, asset as Prefab)); + }, value => { progress.progress = value; }); + this.pending.add(task.cancel); + let prefab: Prefab; + try { prefab = await task.promise; } + finally { this.pending.delete(task.cancel); } + if (this.stopped) throw new Error('Local login resources cancelled'); + if (!isValid(prefab) || !isValid(prefab.data)) throw new Error('Loaded login Prefab dependency is invalid'); const instance = instantiate(prefab); this.loginInstance = instance; instance.parent = this.required(this.loginRoot, 'loginRoot'); @@ -164,10 +174,6 @@ export class LocalPlatformLogin extends Component { }); } - update(deltaSeconds: number): void { - if (!this.stopped && this.spinner !== null && this.spinner.activeInHierarchy) this.spinner.angle -= deltaSeconds * 180; - } - private release(): void { this.stopped = true; for (const cancel of [...this.pending]) cancel(); @@ -206,6 +212,6 @@ export class LocalPlatformLogin extends Component { getDiagnostics() { return Object.freeze({ events: Object.freeze([...this.evidence]), snapshot: this.host?.getSnapshot(), pageModel: this.pageModel, stopped: this.stopped, pendingCount: this.pending.size, - firstDrawAt: this.firstDrawAt, spinnerAngle: this.spinner?.angle }); + firstDrawAt: this.firstDrawAt, resourceProgress: this.resourceProgress?.progress }); } } diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts index 85fba6c..d1484f3 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-view.ts @@ -6,6 +6,48 @@ interface ViewReferences { statusOverlay: Node; statusLabel: Label; resultLabel: Label; } +export interface AssetLoadTask { + readonly promise: Promise; + cancel(): void; +} + +type AssetLoadStarter = ( + onProgress: (completed: number, total: number) => void, + onComplete: (error: unknown, asset?: T) => void, +) => void; + +/** Owns loader callback lifetime so stop/failure cannot revive the login flow. */ +export function createAssetLoadTask(start: AssetLoadStarter, setProgress: (progress: number) => void): AssetLoadTask { + let settled = false; + let resolveTask!: (asset: T) => void; + let rejectTask!: (error: unknown) => void; + const promise = new Promise((resolve, reject) => { resolveTask = resolve; rejectTask = reject; }); + const reject = (error: unknown) => { + if (settled) return; + settled = true; + rejectTask(error); + }; + try { + setProgress(0); + start((completed, total) => { + if (settled) return; + if (!Number.isInteger(completed) || !Number.isInteger(total) || completed < 0 || total <= 0 || completed > total) { + reject(new Error(`Invalid asset load progress: completed=${completed}, total=${total}`)); + return; + } + try { setProgress(completed / total); } + catch (error) { reject(error); } + }, (error, asset) => { + if (settled) return; + if (error !== null && error !== undefined) { reject(error); return; } + if (asset === null || asset === undefined) { reject(new Error('Asset loader completed without an asset')); return; } + settled = true; + resolveTask(asset); + }); + } catch (error) { reject(error); } + return { promise, cancel() { reject(new Error('Asset load cancelled')); } }; +} + export function requireNode(root: Node, path: string): Node { const node = root.getChildByPath(path); if (node === null) throw new Error(`Local login binding missing node: ${path}`); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts index 1dbed17..97ea59f 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/view.test.ts @@ -7,6 +7,7 @@ import { EventEmitter } from 'node:events'; class Sprite {} class UITransform {} +class ProgressBar { progress = 0; } class Label { string = ''; } class Node extends EventEmitter { static EventType = { TOUCH_END: 'touch-end' }; @@ -84,6 +85,53 @@ test('frame request is one shot and real cancellation removes pending draw callb assert.equal(director.listenerCount('draw'), 0); }); +test('asset load task reports validated native loader progress from zero through completion', async () => { + const { createAssetLoadTask } = load(); + const progress: number[] = []; + let onProgress!: (completed: number, total: number) => void; + let onComplete!: (error: unknown, asset?: unknown) => void; + const task = createAssetLoadTask((progressCallback: typeof onProgress, completeCallback: typeof onComplete) => { + onProgress = progressCallback; onComplete = completeCallback; + }, (value: number) => progress.push(value)); + + assert.deepEqual(progress, [0]); + onProgress(1, 2); onProgress(2, 2); onComplete(null, { uuid: 'login' }); + assert.deepEqual(progress, [0, 0.5, 1]); + assert.deepEqual(await task.promise, { uuid: 'login' }); +}); + +test('asset load task rejects invalid counts and loader failures without reporting guessed progress', async () => { + const { createAssetLoadTask } = load(); + for (const invalid of [[0, 0], [-1, 2], [3, 2], [0.5, 2], [1, Number.NaN]]) { + const progress: number[] = []; + let onProgress!: (completed: number, total: number) => void; + const task = createAssetLoadTask((progressCallback: typeof onProgress) => { onProgress = progressCallback; }, + (value: number) => progress.push(value)); + onProgress(invalid[0], invalid[1]); + await assert.rejects(task.promise, /asset load progress/i); + assert.deepEqual(progress, [0]); + } + + let complete!: (error: unknown, asset?: unknown) => void; + const failed = createAssetLoadTask((_progress: unknown, callback: typeof complete) => { complete = callback; }, () => {}); + complete(new Error('decoder failed')); + await assert.rejects(failed.promise, /decoder failed/); +}); + +test('asset load task cancellation settles pending work and ignores late loader callbacks', async () => { + const { createAssetLoadTask } = load(); + const progress: number[] = []; + let onProgress!: (completed: number, total: number) => void; + let onComplete!: (error: unknown, asset?: unknown) => void; + const task = createAssetLoadTask((progressCallback: typeof onProgress, completeCallback: typeof onComplete) => { + onProgress = progressCallback; onComplete = completeCallback; + }, (value: number) => progress.push(value)); + task.cancel(); + await assert.rejects(task.promise, /cancelled/i); + onProgress(1, 2); onComplete(null, { uuid: 'late' }); + assert.deepEqual(progress, [0]); +}); + test('failure display preserves nested server and cleanup errors', () => { const { CocosLocalLoginView } = load(); const f = fixture(); const view = new CocosLocalLoginView(f.refs); @@ -116,14 +164,14 @@ test('component rejects non-debug/native before creating host or writing browser } }); -test('component stop cancels pending draw and timer work and disables spinner updates', async () => { +test('component stop cancels pending draw and timer work', async () => { const path = new URL('../../YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts', import.meta.url); const exports: any = {}; const timers = new Map void>(); let timerId = 0; runInNewContext(ts.transpileModule(readFileSync(path, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, }).outputText, { exports, require: (name: string) => { if (name === 'cc/env') return { DEBUG: true }; - if (name === 'cc') return { Component: class {}, Node, Label, Prefab: class {}, sys: { isNative: false }, + if (name === 'cc') return { Component: class {}, Node, Label, Prefab: class {}, ProgressBar, sys: { isNative: false }, _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; if (name.endsWith('local-login-host.ts')) return {}; if (name.endsWith('local-login-view.ts')) return load(); @@ -136,10 +184,7 @@ test('component stop cancels pending draw and timer work and disables spinner up const component = new exports.LocalPlatformLogin(); const draw = component.nextDraw(); const timer = component.delay(900); const drawRejection = assert.rejects(draw, /cancelled/); const timerRejection = assert.rejects(timer, /cancelled/); - component.spinner = { angle: 50, activeInHierarchy: true }; - component.update(0.5); assert.equal(component.spinner.angle, -40); component.stop(); await Promise.all([drawRejection, timerRejection]); - component.update(0.5); assert.equal(component.spinner.angle, -40); assert.equal(timers.size, 0); assert.equal(director.listenerCount('draw'), 0); assert.equal(component.getDiagnostics().pendingCount, 0); }); @@ -152,7 +197,7 @@ test('minimum display ignores an early timer wake and cancellation clears its re compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, experimentalDecorators: true }, }).outputText, { exports, require: (name: string) => { if (name === 'cc/env') return { DEBUG: true }; - if (name === 'cc') return { Component: class {}, Node, Label, Sprite, UITransform, Prefab: class {}, sys: { isNative: false }, + if (name === 'cc') return { Component: class {}, Node, Label, Sprite, UITransform, ProgressBar, Prefab: class {}, sys: { isNative: false }, _decorator: { ccclass: () => (target: unknown) => target, property: () => () => {} } }; if (name.endsWith('local-login-host.ts')) return { createLocalLoginHost: (options: unknown) => { hostOptions = options; return { start: async () => {}, stop() {}, getSnapshot: () => ({}) }; @@ -168,10 +213,10 @@ test('minimum display ignores an early timer wake and cancellation clears its re async function started() { const component = new exports.LocalPlatformLogin(); const f = fixture(); Object.assign(component, f.refs, { stopButton: new Node(), loadingPrefab: {}, loginPrefab: {} }); - const loading = new Node('Layer614_Loading'); const group = new Node('group-40'); - const spinner = new Node('载入动画'); spinner.components.set(Sprite, new Sprite()); - spinner.components.set(UITransform, new UITransform()); group.children.push(spinner); - loading.children.push(group); component.loadingRoot.children.push(loading); + const logo = new Node('Layer1_Logo'); const group = new Node('group-1'); + const bar = new Node('Spirit254'); bar.components.set(ProgressBar, new ProgressBar()); + group.children.push(bar, new Node('Spirit3103'), new Node('Spirit3104')); + logo.children.push(group, new Node('group-32')); component.loadingRoot.children.push(logo); component.loadingRoot.getComponentsInChildren = () => []; await component.start(); director.emit('draw'); await Promise.resolve(); return component; diff --git a/docs/superpowers/reports/2026-09-05-logo-progress-acceptance.md b/docs/superpowers/reports/2026-09-05-logo-progress-acceptance.md new file mode 100644 index 0000000..6d7ab60 --- /dev/null +++ b/docs/superpowers/reports/2026-09-05-logo-progress-acceptance.md @@ -0,0 +1,35 @@ +# Logo 原生进度条接线验收 + +日期:2026-09-05。分支 codex/local-platform-login,基线41fcd65。用户批准修改现有Layer1_Logo.prefab与LocalPlatformLogin.scene;全部资源操作通过funplay MCP,未手改序列化文本。 + +## 最终行为 + +LocalPlatformLogin 的启动加载实例与loadingPrefab引用改为Layer1_Logo。group-1/Spirit254挂载原生cc.ProgressBar;原有00096_1底图保留,新建Fill子节点引用已有00096_2黄色前景。ProgressBar以FILLED模式控制前景水平填充,totalLength=1、reverse=false、初始progress=0。父节点保持1600×7、原位置和Widget;Fill居中、四边Widget偏移0,避免进度改变宽度与布局竞争。 + +原工程11_GameUI.js:2910的case254按LoadCount裁剪image96第二行。MCP读图确认00096_1是底图、00096_2是前景;因此没有把黑色底图误当填充图。独立资源审查发现过Fill偏移-800的问题,已通过编辑器修正并复审通过。 + +代码通过assetManager.loadAny读取序列化loginPrefab.uuid,使用真实completed/total回调更新进度。缓存命中可能很快从0到1,未伪造中间进度。loader失败、非法计数、停止取消均显式终止,迟到回调不再更新。资源就绪仅在实例化、绑定、Sprite依赖校验与下一帧之后记录;100%不会绕过原有配置、连接和最短展示门。 + +## 边界 + +本批是Logo替换与原生进度条优化,不是完整广告启动流程迁移。诊断实例隐藏Spirit3103/Spirit3104和group-32;保留已有500ms诊断展示门,它不产生资源进度。广告图片、远程launchWaitTime倒计时和跳过仍需后续接线。未改配置来源、协议、服务器、账号、房间或子游戏;未新增RPC验收。LocalPlatformLogin仍为本地诊断入口。 + +## 验证 + +- TDD:新增3个行为测试先RED(8通过/3失败,缺少createAssetLoadTask),实现后聚焦11/11通过;覆盖0/.5/1、非法总数/计数、失败、取消与迟到回调。tsx沙箱ENOMEM后使用相同精确命令在获准环境运行。 +- 显式枚举framework-tests/**/*.test.ts:621/621通过;指定架构测试49/49通过;框架TypeScript与导入边界检查通过。没有运行聚合测试或资源生成器。 +- MCP实际Creator3.8.8主checkout;开始时prefab编辑模式dirty=false。现有50个未跟踪文件SHA256不变,两项历史meta删除保留。 +- Prefab/scene通过编辑器保存。MCP fp_3f9e3aa7b3ad6b20复读保存后的Fill位置0和Widget偏移0;资源引用检查无缺失。 +- 无参数http://localhost:7456/实际网页:fp_d74b6f92c22912ae记录resources-ready先于login页面;最终资源修正后fp_ccac4dd12986039a再次验证ready=true、resourceProgress=1、pendingCount=0、无组件失败,Fill位置0、尺寸1600×7。未点击登录。 +- 正常流程验证后停止host,另做视觉夹具,不将人工设置进度当真实加载证据。1600×765窗口半条与1920×945窗口整条截图均检查;0/.5/1填充比例正确。浏览器遮挡会暂停引擎,验收时恢复可见并等待真实AFTER_DRAW后截图,fp_82f3e3cae88af817确认paused=false、hidden=false、progress=fillRange=1。 +- 临时截图在YouleNexus/temp/mcp-captures/logo-progress-half.png与logo-progress-full-wide.png;未提交生成图。 + +独立任务规格/质量审查及资源修正复审通过。最终整体独立审查通过,无可操作问题。 + +## 补充类型与收尾证据 + +Creator自带run_script_diagnostics(fp_6d1508df1f89941a)在既有webview-javascript-bridge.ts:80的const泛型上报13条语法错误;git show基线41fcd65确认该语法早已存在,本批未改。仓库现代TypeScript直接套Creator旧默认配置另有引擎声明/旧lib兼容诊断,不能将它们当作本批代码回归或宣称默认Creator诊断通过。 + +为覆盖实际Cocos脚本(不只framework),使用现有Creator项目及引擎类型,以命令行指定现代语言检查选项:`node node_modules/typescript/bin/tsc -p YouleNexus/tsconfig.json --noEmit --allowImportingTsExtensions --ignoreDeprecations 6.0 --target ES2020 --lib ES2020,DOM --strict --skipLibCheck --types node`,退出0。没有修改任何tsconfig或引擎文件。 + +最终独立验证窗口停止后stopped=true、ready=false、pendingCount=0并关闭,编辑器dirty=false(fp_eef13b93f09005af)。重新核对原有50个未跟踪文件SHA256不变,2项历史meta删除保留。 \ No newline at end of file