fix: 百人赛关闭按钮位置
This commit is contained in:
@@ -247,6 +247,8 @@ export default class CloudRiseView extends cc.Component {
|
||||
if (!item && !CloudRiseService.unseenResult()) return;
|
||||
}
|
||||
this.overlay = cc.instantiate(prefab);
|
||||
// Keep both drawing and input disabled while page fragments are loading.
|
||||
this.overlay.active = false;
|
||||
this.host.addChild(this.overlay); this.overlay.zIndex = 1001;
|
||||
this.panel = this.overlay.getComponent("CloudRisePanel");
|
||||
}
|
||||
@@ -323,6 +325,7 @@ export default class CloudRiseView extends cc.Component {
|
||||
if (!cc.isValid(this.node, true) || !canOpen()) return;
|
||||
if (locked()) return;
|
||||
this.overlay = cc.instantiate(prefab);
|
||||
this.overlay.active = false;
|
||||
this.host.addChild(this.overlay); this.overlay.zIndex = 1001;
|
||||
this.panel = this.overlay.getComponent("CloudRisePanel");
|
||||
await this.panel.preparePage(pageMode);
|
||||
|
||||
@@ -15,10 +15,18 @@ const PROGRESS_AVATAR_SCALE = 0.9;
|
||||
// 1.2 times the original stage starting stack's visible bounds (341.07 x 201.33).
|
||||
const AVATAR_AREA_WIDTH = 409.28, AVATAR_AREA_HEIGHT = 241.60;
|
||||
const STARTING_STACK_SPREAD = 1.45024;
|
||||
declare const wx: any;
|
||||
|
||||
/** Layout and styles are authored in prefab/CloudRise. This component binds data and actions only. */
|
||||
/** Prefabs own the visual layout; runtime positioning only adjusts close for the native capsule. */
|
||||
@ccclass
|
||||
export default class CloudRisePanel extends cc.Component {
|
||||
private static capsuleCache: { width: number; height: number; bottom: number } = null;
|
||||
private closeOverviewY = 600;
|
||||
private closePageY = 920;
|
||||
private safeLayoutListening = false;
|
||||
private safeLayoutPending = false;
|
||||
private safeLayoutRetries = 0;
|
||||
private openAfterLayout = false;
|
||||
@property(cc.Node) failureMessage: cc.Node = null;
|
||||
@property(cc.Node) expiredMessage: cc.Node = null;
|
||||
@property([cc.Node]) progressStatsContents: cc.Node[] = [];
|
||||
@@ -181,7 +189,10 @@ export default class CloudRisePanel extends cc.Component {
|
||||
this.cloudFloatTime = 0;
|
||||
}
|
||||
|
||||
onDisable(): void { this.resetCloudFloat(); this.stopVictorySparkles(); this.stopOverviewSparkles(); this.hideRewardHint(); this.showGuide(false); }
|
||||
onDisable(): void {
|
||||
if (this.safeLayoutPending) return; // A hidden geometry pass must preserve the current page state.
|
||||
this.resetCloudFloat(); this.stopVictorySparkles(); this.stopOverviewSparkles(); this.hideRewardHint(); this.showGuide(false);
|
||||
}
|
||||
|
||||
private onExpired: () => void = null;
|
||||
initialize(mode: string, _inGame: boolean, close: () => void, _play: () => void, onExpired?: () => void): void {
|
||||
@@ -201,7 +212,82 @@ export default class CloudRisePanel extends cc.Component {
|
||||
if (!this.busy) this.render();
|
||||
});
|
||||
this.schedule(this.tick, 1); this.render(); this.warmMatching();
|
||||
if (this.mode === "overview") this.openPopup();
|
||||
if (typeof wx !== "undefined") {
|
||||
this.openAfterLayout = this.mode === "overview";
|
||||
this.safeLayoutListening = true;
|
||||
cc.view.on("canvas-resize", this.queueSafeLayout, this);
|
||||
cc.game.on(cc.game.EVENT_SHOW, this.queueSafeLayout, this);
|
||||
this.queueSafeLayout();
|
||||
} else {
|
||||
this.node.active = true;
|
||||
if (this.mode === "overview") this.openPopup();
|
||||
}
|
||||
}
|
||||
|
||||
/** Inactive roots neither draw nor receive input, including during the bounded retry. */
|
||||
private queueSafeLayout(): void {
|
||||
if (this.safeLayoutPending) return;
|
||||
this.safeLayoutPending = true; this.safeLayoutRetries = 0;
|
||||
this.node.active = false;
|
||||
cc.director.once(cc.Director.EVENT_BEFORE_DRAW, this.finishSafeLayout, this);
|
||||
}
|
||||
private finishSafeLayout(): void {
|
||||
if (!cc.isValid(this.node, true)) return;
|
||||
let info: any = {}, rect: any = null;
|
||||
try { info = (wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()) || {}; } catch (_) { }
|
||||
try { rect = wx.getMenuButtonBoundingClientRect && wx.getMenuButtonBoundingClientRect(); } catch (_) { }
|
||||
const frame = cc.view.getFrameSize();
|
||||
const width = Number(info.windowWidth) || frame.width, height = Number(info.windowHeight) || frame.height;
|
||||
const valid = rect && [rect.left, rect.right, rect.top, rect.bottom].every(Number.isFinite)
|
||||
&& rect.left >= 0 && rect.right > rect.left && rect.right <= width
|
||||
&& rect.top >= 0 && rect.bottom > rect.top && rect.bottom < height;
|
||||
if (valid) CloudRisePanel.capsuleCache = { width, height, bottom: rect.bottom };
|
||||
const cache = CloudRisePanel.capsuleCache;
|
||||
const cached = cache && cache.width === width && cache.height === height;
|
||||
if (!valid && !cached && this.safeLayoutRetries++ < 1) {
|
||||
cc.director.once(cc.Director.EVENT_BEFORE_DRAW, this.finishSafeLayout, this);
|
||||
return;
|
||||
}
|
||||
// Only a hidden layout pass reads platform geometry. Data refreshes reuse these positions.
|
||||
const bottom = valid ? rect.bottom : cached ? cache.bottom
|
||||
: Math.max(Number(info.safeArea && info.safeArea.top) || 0, Number(info.statusBarHeight) || 0, 44) + 48;
|
||||
cc.Tween.stopAllByTarget(this.design); this.popupAnimating = false;
|
||||
const size = cc.view.getVisibleSize();
|
||||
this.node.setContentSize(size); this.background.setContentSize(size);
|
||||
this.popupScale = Math.min(size.width / this.design.width, size.height / this.design.height);
|
||||
this.design.scale = this.popupScale;
|
||||
// Flush authored alignment before measuring, including ancestors outside the activity.
|
||||
const ancestors: cc.Node[] = [];
|
||||
for (let node = this.node; node; node = node.parent) ancestors.unshift(node);
|
||||
ancestors.forEach(node => { const widget = node.getComponent(cc.Widget); if (widget && widget.enabled) widget.updateAlignment(); });
|
||||
this.node.getComponentsInChildren(cc.Widget).forEach(widget => { if (widget.enabled) widget.updateAlignment(); });
|
||||
const viewport = cc.view.getViewportRect();
|
||||
// Convert logical window pixels to engine screen coordinates, accounting for retina and viewport offsets.
|
||||
const safeScreenY = (cc.game.canvas.height * (1 - (bottom + 10) / height) - viewport.y) / cc.view.getScaleY();
|
||||
this.closePageY = this.safeCloseY(920, safeScreenY, 1);
|
||||
// backOut overshoots the 8/7 target: the actual maximum is < 1.25 of the final scale.
|
||||
this.closeOverviewY = this.safeCloseY(600, safeScreenY, 1.25);
|
||||
this.design.scale = this.popupScale;
|
||||
this.applyClosePosition();
|
||||
this.safeLayoutPending = false;
|
||||
this.node.active = true;
|
||||
if (this.openAfterLayout && this.mode === "overview") this.openPopup();
|
||||
this.openAfterLayout = false;
|
||||
}
|
||||
private safeCloseY(authoredY: number, screenY: number, animationScale: number): number {
|
||||
const button = this.buttons[0];
|
||||
this.design.scale = this.popupScale * animationScale;
|
||||
const camera = cc.Camera.findCamera(button);
|
||||
const world = button.parent.convertToWorldSpaceAR(cc.v2(button.x, authoredY));
|
||||
const screen = camera ? camera.getWorldToScreenPoint(world) : world;
|
||||
screen.y = screenY;
|
||||
const limit = button.parent.convertToNodeSpaceAR(camera ? camera.getScreenToWorldPoint(screen) : screen);
|
||||
const control = button.getComponent(cc.Button);
|
||||
const top = button.height * (1 - button.anchorY) * Math.abs(button.scaleY) * Math.max(1, control ? control.zoomScale : 1);
|
||||
return Math.min(authoredY, limit.y - top);
|
||||
}
|
||||
private applyClosePosition(): void {
|
||||
if (this.buttons[0]) this.buttons[0].y = this.mode === "overview" ? this.closeOverviewY : this.closePageY;
|
||||
}
|
||||
acknowledge(): void { if (this.mode === "result") Service.acknowledgeResult(); }
|
||||
private canContinue(): boolean {
|
||||
@@ -598,7 +684,7 @@ export default class CloudRisePanel extends cc.Component {
|
||||
this.buttons[1].active = this.mode === "overview";
|
||||
if (this.buttons[2]) this.buttons[2].active = false;
|
||||
if (this.buttons[4]) this.buttons[4].active = this.mode === "participants";
|
||||
this.buttons[0].y = this.mode === "overview" ? 600 : 920;
|
||||
this.applyClosePosition();
|
||||
this.errorLabel.string = Service.error;
|
||||
this.errorLabel.node.active = !!Service.error && !this.busy;
|
||||
this.buttons[5].active = this.errorLabel.node.active;
|
||||
@@ -934,7 +1020,7 @@ export default class CloudRisePanel extends cc.Component {
|
||||
private updateEliminationClose(): void {
|
||||
if (!this.buttons[0]) return;
|
||||
this.buttons[0].active = this.eliminationSkippable;
|
||||
this.buttons[0].y = 920;
|
||||
this.buttons[0].y = this.closePageY;
|
||||
}
|
||||
async playElimination(item: any): Promise<boolean> {
|
||||
this.busy = true;
|
||||
@@ -1051,6 +1137,11 @@ export default class CloudRisePanel extends cc.Component {
|
||||
}
|
||||
}
|
||||
onDestroy(): void {
|
||||
if (this.safeLayoutListening) {
|
||||
cc.view.off("canvas-resize", this.queueSafeLayout, this);
|
||||
cc.game.off(cc.game.EVENT_SHOW, this.queueSafeLayout, this);
|
||||
cc.director.off(cc.Director.EVENT_BEFORE_DRAW, this.finishSafeLayout, this);
|
||||
}
|
||||
this.stopOverviewSparkles();
|
||||
if (this.design) cc.Tween.stopAllByTarget(this.design);
|
||||
if (this.background) cc.Tween.stopAllByTarget(this.background);
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const ts = require('typescript');
|
||||
const source = ts.transpileModule(fs.readFileSync('assets/cloud_rise_home/scripts/CloudRisePanel.ts', 'utf8'), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017, experimentalDecorators: true }
|
||||
}).outputText;
|
||||
|
||||
function fixture(options = {}) {
|
||||
const state = { width: 393, height: 852, bottom: 92, dpr: 3, viewportY: 0, cameraY: 0, ...options };
|
||||
function emitter() {
|
||||
const listeners = [];
|
||||
return {
|
||||
on(event, fn, target) { listeners.push({ event, fn, target }); },
|
||||
once(event, fn, target) { listeners.push({ event, fn, target, once: true }); },
|
||||
off(event, fn, target) { for (let i = listeners.length - 1; i >= 0; i--) if (listeners[i].event === event && listeners[i].fn === fn && listeners[i].target === target) listeners.splice(i, 1); },
|
||||
emit(event) { for (const l of listeners.slice()) if (l.event === event) { if (l.once) this.off(event, l.fn, l.target); l.fn.call(l.target); } },
|
||||
count: () => listeners.length
|
||||
};
|
||||
}
|
||||
class Node {
|
||||
constructor(parent = null) { this.parent = parent; this.children = []; if (parent) parent.children.push(this); this.x = this.y = 0; this.scale = 1; this.active = true; this.width = 1080; this.height = 2200; this.anchorY = 0.5; }
|
||||
get scaleY() { return this.scale; }
|
||||
setContentSize(size) { this.width = size.width; this.height = size.height; }
|
||||
convertToWorldSpaceAR(p) { const q = { x: this.x + p.x * this.scale, y: this.y + p.y * this.scale }; return this.parent ? this.parent.convertToWorldSpaceAR(q) : q; }
|
||||
convertToNodeSpaceAR(p) { const q = this.parent ? this.parent.convertToNodeSpaceAR(p) : p; return { x: (q.x - this.x) / this.scale, y: (q.y - this.y) / this.scale }; }
|
||||
getComponent(type) { return type === cc.Button ? { zoomScale: 1.03 } : null; }
|
||||
getComponentsInChildren() { return []; }
|
||||
on() {} off() {}
|
||||
}
|
||||
const director = emitter(), game = Object.assign(emitter(), { EVENT_SHOW: 'show', canvas: {} });
|
||||
const engineScale = () => state.width * state.dpr / 1080;
|
||||
const view = Object.assign(emitter(), {
|
||||
getVisibleSize: () => ({ width: 1080, height: state.height * 1080 / state.width }),
|
||||
getFrameSize: () => ({ width: state.width, height: state.height }),
|
||||
getViewportRect: () => ({ x: 0, y: state.viewportY }), getScaleY: engineScale
|
||||
});
|
||||
Object.defineProperty(game.canvas, 'height', { get: () => state.height * state.dpr });
|
||||
const camera = { getWorldToScreenPoint: p => ({ x: p.x, y: p.y - state.cameraY }), getScreenToWorldPoint: p => ({ x: p.x, y: p.y + state.cameraY }) };
|
||||
const cc = { _decorator: { ccclass: c => c, property: () => () => {} }, Component: class { schedule() {} }, Node,
|
||||
Label: class {}, Sprite: class {}, SpriteFrame: class {}, String, Widget: class {}, Button: class {},
|
||||
Camera: { findCamera: () => camera }, v2: (x, y) => ({ x, y }), view, game, director,
|
||||
Director: { EVENT_BEFORE_DRAW: 'draw' }, Tween: { stopAllByTarget() {} }, isValid: n => !!n && !n.destroyed };
|
||||
let reads = 0;
|
||||
const wx = {
|
||||
getWindowInfo: () => ({ windowWidth: state.width, windowHeight: state.height, safeArea: { top: 59 } }),
|
||||
getMenuButtonBoundingClientRect() { reads++; if (state.error) throw Error('not ready'); return state.bottom ? { left: state.width - 100, right: state.width - 8, top: state.bottom - 32, bottom: state.bottom } : {}; }
|
||||
};
|
||||
const exports = {};
|
||||
vm.runInNewContext(source, { exports, cc, wx, console, require: () => ({ default: { subscribe: () => () => {} } }) });
|
||||
const parent = new Node(); parent.x = 540; parent.y = view.getVisibleSize().height / 2;
|
||||
const panel = new exports.default(); panel.node = new Node(parent); panel.node.active = false;
|
||||
panel.background = new Node(panel.node); panel.design = new Node(panel.node);
|
||||
const button = new Node(panel.design); button.x = 395; button.y = 920; button.width = 103; button.height = 112;
|
||||
panel.buttons = [button];
|
||||
panel.bindButtons = () => {}; panel.warmMatching = () => {};
|
||||
panel.render = () => panel.applyClosePosition();
|
||||
let openings = 0;
|
||||
panel.openPopup = () => { openings++; assert(panel.node.active); };
|
||||
function top(zoom = 1.03) {
|
||||
const world = button.convertToWorldSpaceAR({ x: 0, y: button.height * (1 - button.anchorY) * zoom });
|
||||
return state.height - (camera.getWorldToScreenPoint(world).y * engineScale() + state.viewportY) / state.dpr;
|
||||
}
|
||||
return { panel, button, state, cc, wx, parent, reads: () => reads, openings: () => openings, top,
|
||||
draw: () => director.emit('draw'), init: (mode = 'progress') => panel.initialize(mode, false, () => {}, () => {}) };
|
||||
}
|
||||
|
||||
for (const device of [{ width: 393, height: 852, bottom: 92, dpr: 3 }, { width: 400, height: 890, bottom: 88, dpr: 2.75 }, { width: 375, height: 667, bottom: 64, dpr: 2 }]) {
|
||||
test(`first visible frame clears capsule and pressed bounds on ${device.width}x${device.height}`, () => {
|
||||
const f = fixture(device); f.init(); assert.equal(f.panel.node.active, false);
|
||||
f.draw(); assert.equal(f.panel.node.active, true);
|
||||
assert(f.top() >= device.bottom + 10 - 1e-8, `top=${f.top()}`);
|
||||
const y = f.button.y, reads = f.reads(); f.state.bottom += 20;
|
||||
for (let i = 0; i < 20; i++) { f.panel.render(); f.draw(); }
|
||||
assert.equal(f.button.y, y); assert.equal(f.reads(), reads, 'ordinary refresh never repositions from late API data');
|
||||
});
|
||||
}
|
||||
|
||||
test('viewport offset, camera translation, nested scale and noncentral anchor are converted before reveal', () => {
|
||||
const f = fixture({ viewportY: -85, cameraY: 43 });
|
||||
f.parent.scale = 0.85; f.button.anchorY = 0.2;
|
||||
f.init(); f.draw(); assert(f.top() >= 102 - 1e-8);
|
||||
});
|
||||
|
||||
test('overview position remains safe for the complete backOut and pressed-button envelope', () => {
|
||||
const f = fixture({ bottom: 220 }); f.init('overview');
|
||||
assert.equal(f.openings(), 0); f.draw(); assert.equal(f.openings(), 1);
|
||||
const finalScale = f.panel.popupScale;
|
||||
for (let i = 0; i <= 100; i++) {
|
||||
const t = i / 100 - 1, backOut = t * t * (2.70158 * t + 1.70158) + 1;
|
||||
f.panel.design.scale = finalScale * (1 / 7 + backOut);
|
||||
assert(f.top() >= 230 - 1e-8);
|
||||
}
|
||||
f.panel.design.scale = finalScale;
|
||||
});
|
||||
|
||||
test('invalid capsule retries while invisible, then succeeds without a visible correction', () => {
|
||||
const f = fixture({ bottom: 0 }); f.init(); f.draw();
|
||||
assert.equal(f.panel.node.active, false);
|
||||
f.state.bottom = 92; f.draw(); assert.equal(f.panel.node.active, true); assert(f.top() >= 102 - 1e-8);
|
||||
});
|
||||
|
||||
test('persistent API errors reveal a stable conservative fallback after one retry', () => {
|
||||
const f = fixture({ error: true }); f.init(); f.draw(); f.draw();
|
||||
assert.equal(f.panel.node.active, true); assert(f.top() >= 59 + 48 + 10 - 1e-8);
|
||||
const y = f.button.y; f.state.error = false; f.panel.render(); f.draw(); assert.equal(f.button.y, y);
|
||||
});
|
||||
|
||||
test('resume uses matching cache; resize invalidates it and relayouts hidden without accumulating offsets', () => {
|
||||
const f = fixture(); f.init(); f.draw(); const y = f.button.y;
|
||||
f.state.error = true; f.cc.game.emit('show'); assert.equal(f.panel.node.active, false); f.draw();
|
||||
assert.equal(f.panel.node.active, true); assert.equal(f.button.y, y);
|
||||
f.state.height = 800; f.cc.view.emit('canvas-resize'); f.draw(); assert.equal(f.panel.node.active, false);
|
||||
f.draw(); assert.equal(f.panel.node.active, true); assert(f.top() >= 117 - 1e-8);
|
||||
f.state.error = false; f.state.height = 852; f.cc.view.emit('canvas-resize'); f.draw(); assert.equal(f.button.y, y);
|
||||
});
|
||||
|
||||
test('settlement close and later page refreshes share the same safe position', () => {
|
||||
const f = fixture(); f.init(); f.draw(); const y = f.button.y;
|
||||
f.panel.mode = 'elimination'; f.panel.eliminationSkippable = true; f.panel.updateEliminationClose();
|
||||
assert.equal(f.button.y, y); assert.equal(f.button.active, true);
|
||||
f.panel.mode = 'overview'; f.panel.render(); f.panel.mode = 'progress'; f.panel.render(); assert.equal(f.button.y, y);
|
||||
});
|
||||
|
||||
test('destroying a hidden pending page removes draw, resize and resume callbacks', () => {
|
||||
const f = fixture(); f.init(); f.panel.onDestroy();
|
||||
assert.equal(f.cc.director.count(), 0); assert.equal(f.cc.view.count(), 0); assert.equal(f.cc.game.count(), 0);
|
||||
f.draw(); assert.equal(f.panel.node.active, false);
|
||||
});
|
||||
|
||||
test('temporary hiding for relayout preserves guide and cloud animation state', () => {
|
||||
const f = fixture(); f.init(); f.draw();
|
||||
f.panel.cloudFloatTime = 2;
|
||||
let hidden = 0; f.panel.showGuide = () => hidden++;
|
||||
f.cc.game.emit('show'); f.panel.onDisable();
|
||||
assert.equal(f.panel.cloudFloatTime, 2); assert.equal(hidden, 0);
|
||||
});
|
||||
|
||||
test('opening the activity attaches an inactive root throughout asynchronous fragment loading', async () => {
|
||||
let release, attached;
|
||||
const panel = {
|
||||
preparePage: () => new Promise(resolve => { release = resolve; }),
|
||||
initialize() { assert.equal(attached.active, false); attached.initialized = true; }
|
||||
};
|
||||
const service = { sync: async () => true, dismissExpiryPresentation() {}, visible: () => true,
|
||||
lockedUntilLevel: () => 0, retainResources: () => () => {}, pagePrefab: async () => ({}),
|
||||
unseenResult: () => false, run: () => ({ status: 'playing' }), stage: () => ({ stage: 1 }), pageFragment: async () => ({}) };
|
||||
const cc = { _decorator: { ccclass: c => c }, Component: class {}, isValid: () => true,
|
||||
instantiate: () => ({ active: true, getComponent: () => panel }), warn() {} };
|
||||
const exports = {};
|
||||
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/Script/CloudRiseView.ts', 'utf8'), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017, experimentalDecorators: true }
|
||||
}).outputText, { exports, cc, console, require: name => name.endsWith('CloudRiseService') ? { default: service }
|
||||
: { default: {}, MiniGameSdk: { API: { showToast(message) { assert.fail(message); } } } } });
|
||||
const view = new exports.default(); view.node = {}; view.host = { addChild(node) { assert.equal(node.active, false); attached = node; } };
|
||||
const task = view.open('progress');
|
||||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||||
assert(attached); assert.equal(attached.active, false); assert.equal(attached.initialized, undefined);
|
||||
release(); await task; assert.equal(attached.initialized, true);
|
||||
});
|
||||
Reference in New Issue
Block a user