diff --git a/assets/Script/SceneManager.ts b/assets/Script/SceneManager.ts index 854e48d2..365e7cc9 100644 --- a/assets/Script/SceneManager.ts +++ b/assets/Script/SceneManager.ts @@ -2324,7 +2324,11 @@ export default class SceneManager extends cc.Component { // this.node.addChild(reddemCodeNode); } update(dt) { - + if (this.threeWinsPanel) { + this.threeWinsPanel.refreshInventory(this.threeWinsData()); + const transition = this.node.getChildByName("zhuanchang"); + if (!transition || !transition.active) this.threeWinsPanel.playEntry(); + } } public setPic(url, node) { diff --git a/assets/Script/StartBoostOpening.ts b/assets/Script/StartBoostOpening.ts index 7512dae4..f0818b67 100644 --- a/assets/Script/StartBoostOpening.ts +++ b/assets/Script/StartBoostOpening.ts @@ -3,10 +3,14 @@ import { applyRocketCut } from "./RocketCut"; import NumberToImage from "./NumberToImage"; import { StartBoostLoadout, StartBoostProp, startBoostRewards } from "./StartBoostLoadout"; -const ENTRY_SECONDS = 0.9; +const ENTRY_SECONDS = 0.65; const ENTRY_HOLD_SECONDS = 0.3; +const LAUNCH_CLOUD_SECONDS = 0.65; const HOURGLASS_SPEED = 1100; -const ROCKET_SPEED = 700; +const ROCKET_SPEED = 950; +const ROCKET_ARC_HEIGHT = 480; +// 复用炸弹的爆炸段,跳过火箭命中后不需要的引爆前摇。 +const ROCKET_IMPACT_START_SECONDS = 0.3333; const HOURGLASS_FLIGHT_SCALE = 0.7; function flightPoint(from: cc.Vec2, control: cc.Vec2, to: cc.Vec2, t: number): cc.Vec2 { @@ -145,7 +149,7 @@ export default class StartBoostOpening { if (this.elapsed >= ENTRY_SECONDS + ENTRY_HOLD_SECONDS) this.beginFlights(); return false; } - this.updateLaunchCloud(Math.min(1, this.elapsed / 0.25), true); + this.updateLaunchCloud(Math.min(1, this.elapsed / LAUNCH_CLOUD_SECONDS), true); this.updateTimeDisplay(dt); for (const flight of this.flights) { if (flight.landed) { @@ -178,7 +182,7 @@ export default class StartBoostOpening { node.convertToWorldSpaceAR(cc.v2(0, rocket ? -node.height : 0)))); if (t === 1) this.land(flight); } - return this.elapsed >= 0.25 && this.displayedTime === this.timeTarget && + return this.elapsed >= LAUNCH_CLOUD_SECONDS && this.displayedTime === this.timeTarget && this.carriedEntries.every(entry => this.elapsed + ENTRY_HOLD_SECONDS >= entry.trail.life + entry.trail.lifeVar) && this.flights.every(flight => flight.landed && flight.impactElapsed >= flight.impactSeconds); } @@ -257,7 +261,7 @@ export default class StartBoostOpening { node.y + node.height * node.scaleY / 2 - 8); } const from = cc.v2(node.x, node.y); - const control = shot ? cc.v2((from.x + to.x) / 2, Math.max(from.y, to.y) + 600) + const control = shot ? cc.v2((from.x + to.x) / 2, Math.max(from.y, to.y) + ROCKET_ARC_HEIGHT) // 沙漏交替绕左、绕右,后续沙漏使用更宽、不同高度的弧线,最后汇入时钟。 : cc.v2((from.x + to.x) / 2 + (i % 2 === 0 ? -1 : 1) * (240 + Math.floor(i / 2) * 180), from.y + (to.y - from.y) * (0.4 + i * 0.12)); @@ -280,7 +284,7 @@ export default class StartBoostOpening { rocketDistances.set(shot.origin, distances[distances.length - 1]); } this.flights.push({ node, shot, to, from, control, distances, trail, timeText, - landed: false, impact: null, impactElapsed: 0, impactSeconds: 0.8 }); + landed: false, impact: null, impactElapsed: 0, impactSeconds: trail.life + trail.lifeVar }); if (this.carriedEntries.some(entry => entry.node === node)) this.onUseCarried(shot ? "rocket" : "hourglass"); } } @@ -304,7 +308,8 @@ export default class StartBoostOpening { this.root.addChild(flight.impact); flight.impact.setPosition(flight.to); const track = skeleton.setAnimation(0, "eff", false); - flight.impactSeconds = Math.max(0.8, track.animation.duration); + track.trackTime = Math.min(ROCKET_IMPACT_START_SECONDS, track.animation.duration); + flight.impactSeconds = Math.max(flight.impactSeconds, track.animation.duration - track.trackTime); const shot = flight.shot; const id = this.map.mapBlocksWall[shot.x][shot.y].getComponent("MapBlock").block_Id; const block = this.map.blocks.find(node => node.uuid === id).getComponent("Block"); @@ -327,10 +332,10 @@ export default class StartBoostOpening { const size = 64, pixels = new Uint8Array(size * size * 4); for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { const radius = Math.hypot(x + 0.5 - size / 2, y + 0.5 - size / 2) / (size / 2); - const edge = Math.min(1, Math.max(0, (1 - radius) / 0.3)); + const softness = Math.max(0, 1 - radius * radius); const offset = (y * size + x) * 4; pixels[offset] = pixels[offset + 1] = pixels[offset + 2] = 255; - pixels[offset + 3] = Math.round(255 * edge * edge * (3 - 2 * edge)); + pixels[offset + 3] = Math.round(160 * softness * softness); } this.cloudTexture = new cc.Texture2D(); this.cloudTexture.initWithData(pixels, cc.Texture2D.PixelFormat.RGBA8888, size, size); @@ -356,13 +361,19 @@ export default class StartBoostOpening { } this.launchCloud.active = !(dispersing && progress === 1); if (!this.launchCloud.active) return; - this.launchCloud.opacity = Math.round(255 * (dispersing ? 1 - progress : Math.min(1, progress * 2))); + this.launchCloud.opacity = Math.round(255 * (dispersing ? 1 : Math.min(1, progress * 2))); this.launchCloud.children.forEach(cloud => cloud.children.forEach((puff, i) => { - const radius = i === 0 ? 0 : i <= 6 ? 55 : 110; + const layer = i === 0 ? 0 : i <= 6 ? 1 : 2; + const radius = layer * 55; const angle = i <= 6 ? (i - 1) * Math.PI / 3 : (i - 7) * Math.PI / 6; - const spread = dispersing ? 1 + progress * 0.15 : 0.6 + progress * 0.4; + // 外层先散,内层稍后散;同层也略微错开,避免整团一起消失。 + const delay = (2 - layer) * 0.1 + i % 3 * 0.025; + const phase = dispersing ? Math.min(1, Math.max(0, (progress - delay) / (0.8 - layer * 0.1))) : 0; + const fade = phase * phase * (3 - 2 * phase); + puff.opacity = Math.round((155 - layer * 20 - i % 3 * 9) * (1 - fade)); + const spread = dispersing ? 1 + fade * (0.07 + layer * 0.04) : 0.6 + progress * 0.4; puff.setPosition(Math.cos(angle) * radius * spread, Math.sin(angle) * radius * spread); - puff.setScale((2.5 + i % 3 * 0.05) * (dispersing ? 1 + progress * 0.1 : 0.5 + progress * 0.5)); + puff.setScale((2.5 + i % 3 * 0.05) * (dispersing ? 1 + fade * 0.1 : 0.5 + progress * 0.5)); })); } diff --git a/assets/Script/ThreeWinStreak.ts b/assets/Script/ThreeWinStreak.ts index 00a92cd2..62444892 100644 --- a/assets/Script/ThreeWinStreak.ts +++ b/assets/Script/ThreeWinStreak.ts @@ -14,6 +14,10 @@ export default class ThreeWinStreak { /** 无参数先恢复本地;登录后传服务器数据,旧账号缺失字段从零开始。 */ static restore(serverData?: { threeWinStreak?: number }, forced = false) { + if (cc.fx.GameConfig.forceThreeWinStreak) { + cc.fx.GameConfig.GM_INFO.threeWinStreak = 3; + return; + } const storage = cc.fx.StorageMessage; if (serverData && forced) { storage.setStorage("threeWinStreakPending", false); @@ -37,6 +41,7 @@ export default class ThreeWinStreak { begin() { if (!this.enabled || this.started || this.settled) return; this.started = true; + if (cc.fx.GameConfig.forceThreeWinStreak) return; cc.fx.StorageMessage.setStorage("threeWinStreakPending", true); } @@ -52,6 +57,10 @@ export default class ThreeWinStreak { private finish(value: number) { this.settled = true; + if (cc.fx.GameConfig.forceThreeWinStreak) { + cc.fx.GameConfig.GM_INFO.threeWinStreak = 3; + return; + } const count = ThreeWinStreak.count(value); const storage = cc.fx.StorageMessage; cc.fx.GameConfig.GM_INFO.threeWinStreak = count; diff --git a/assets/Script/ThreeWinsPanel.ts b/assets/Script/ThreeWinsPanel.ts index d4978cd2..fc58d065 100644 --- a/assets/Script/ThreeWinsPanel.ts +++ b/assets/Script/ThreeWinsPanel.ts @@ -32,6 +32,7 @@ export default class ThreeWinsPanel extends cc.Component { private callbacks: ThreeWinsPanelCallbacks = null; private selected: ThreeWinsSelection = { hourglass: false, rocket: false }; private bound = false; + private entryScale: cc.Vec2 = null; init(atlas: cc.SpriteAtlas, data: ThreeWinsPanelData, callbacks: ThreeWinsPanelCallbacks) { this.atlas = atlas; @@ -44,6 +45,19 @@ export default class ThreeWinsPanel extends cc.Component { this.bound = true; } this.refresh(data); + this.entryScale = cc.v2(this.node.scaleX, this.node.scaleY); + this.node.setScale(0); + } + + playEntry() { + if (!this.entryScale) return; + const scale = this.entryScale; + this.entryScale = null; + cc.tween(this.node) + .to(0.28, { scaleX: scale.x * 1.06, scaleY: scale.y * 1.06 }, { easing: "cubicOut" }) + .to(0.10, { scaleX: scale.x * 0.97, scaleY: scale.y * 0.97 }, { easing: "sineInOut" }) + .to(0.12, { scaleX: scale.x, scaleY: scale.y }, { easing: "sineOut" }) + .start(); } refresh(data: ThreeWinsPanelData) { @@ -64,6 +78,17 @@ export default class ThreeWinsPanel extends cc.Component { this.renderProp("rocket"); } + refreshInventory(data: ThreeWinsPanelData) { + const previous = this.data; + this.data = data; + if (!previous || previous.hourglassAmount !== data.hourglassAmount || previous.infiniteHourglass !== data.infiniteHourglass) { + this.renderProp("hourglass"); + } + if (!previous || previous.rocketAmount !== data.rocketAmount || previous.infiniteRocket !== data.infiniteRocket) { + this.renderProp("rocket"); + } + } + setSelected(prop: ThreeWinsProp, selected: boolean) { this.selected[prop] = selected; this.renderProp(prop); @@ -83,29 +108,24 @@ export default class ThreeWinsPanel extends cc.Component { private renderProp(prop: ThreeWinsProp) { const node = this.node.getChildByName(prop); - const selected = this.selected[prop]; - node.getChildByName("add").active = !selected; - node.getChildByName("select").active = selected; - - let amount = node.getChildByName("amount"); - if (!amount) { - amount = new cc.Node("amount"); - amount.parent = node; - amount.y = -140; - } + const count = prop === "hourglass" ? this.data.hourglassAmount : this.data.rocketAmount; const infinite = prop === "hourglass" ? this.data.infiniteHourglass : this.data.infiniteRocket; - if (!selected && infinite) { + const available = count > 0 || !!infinite; + if (!available) this.selected[prop] = false; + const selected = this.selected[prop]; + node.getChildByName("add").active = !available; + node.getChildByName("select").active = selected; + const remaining = node.getChildByName("remaining"); + remaining.active = available && !selected; + + const amount = remaining.getChildByName("number"); + if (infinite) { this.clearNumber(amount); this.addCountSymbol(amount, "∞", 0); return; } - const count = selected ? 1 : prop === "hourglass" ? this.data.hourglassAmount : this.data.rocketAmount; this.renderNumber(amount, count, "prop_", 30); - if (selected) { - for (const digit of amount.children) digit.x += 13; - this.addCountSymbol(amount, "+", -17); - } } private renderNumber(target: cc.Node, value: number, prefix: string, spacing: number, right = false) { diff --git a/assets/Script/module/Config/GameConfig.ts b/assets/Script/module/Config/GameConfig.ts index 5230f18a..74d465d4 100644 --- a/assets/Script/module/Config/GameConfig.ts +++ b/assets/Script/module/Config/GameConfig.ts @@ -444,6 +444,9 @@ export class GameConfig { // 临时老彩虹测试开关(默认关闭):开启后强制 C、立即解锁连胜、始终十连胜。 static forceOldRainbowWinStreakC = false; + // 临时测试:本地始终保持三连胜,不写入真实存档;false 恢复正常规则。 + static forceThreeWinStreak = false; + static isStartBoostersUnlocked(): boolean { return this.GM_INFO.level + 1 >= 25 && !(this.GM_INFO.otherLevel > 0); } @@ -826,7 +829,7 @@ export class GameConfig { popPassCheck: false, // 弹窗通行证 passCheckActivate: false, winStreak: (this.forceOldRainbowWinStreakC || this.forceRainbowWinStreakB) ? 10 : 0, //测试模式默认十连胜 - threeWinStreak: 0, + threeWinStreak: this.forceThreeWinStreak ? 3 : 0, tasks: { levelPass: { value: 0, target: 0, state: 0 }, share: { value: 0, target: 0, state: 0 }, useEnergy: { value: 0, target: 0, state: 0 }, useProp: { value: 0, target: 0, state: 0 } }, addLevel: 0, //每日过关数 address: "", //用户城市信息 diff --git a/assets/prefab/pop/threeWins.prefab b/assets/prefab/pop/threeWins.prefab index c545fe48..97dbe108 100644 --- a/assets/prefab/pop/threeWins.prefab +++ b/assets/prefab/pop/threeWins.prefab @@ -78,17 +78,17 @@ "__id__": 96 }, { - "__id__": 113 + "__id__": 118 } ], "_active": true, "_components": [ { - "__id__": 129 + "__id__": 139 } ], "_prefab": { - "__id__": 130 + "__id__": 140 }, "_opacity": 255, "_color": { @@ -3568,19 +3568,22 @@ }, { "__id__": 103 + }, + { + "__id__": 109 } ], "_active": true, "_components": [ { - "__id__": 109 + "__id__": 114 }, { - "__id__": 110 + "__id__": 115 } ], "_prefab": { - "__id__": 112 + "__id__": 117 }, "_opacity": 255, "_color": { @@ -4061,6 +4064,188 @@ "fileId": "094FrFsXlHSpY6XaV7MnZR", "sync": false }, + { + "__type__": "cc.Node", + "_name": "remaining", + "_objFlags": 0, + "_parent": { + "__id__": 96 + }, + "_children": [ + { + "__id__": 110 + } + ], + "_active": false, + "_components": [ + { + "__id__": 112 + } + ], + "_prefab": { + "__id__": 113 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 68, + "height": 74 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 78.667, + -84.617, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "number", + "_objFlags": 0, + "_parent": { + "__id__": 109 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 111 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 68, + "height": 74 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "dfI6RjAeZEeJGfLVCol5KL", + "sync": false + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 109 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "d45452ee-6ac4-4adc-ad49-cd876b55589f" + }, + "_type": 0, + "_sizeMode": 1, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "88on1lyRRMK6WGzsyIYY78", + "sync": false + }, { "__type__": "cc.Sprite", "_name": "", @@ -4107,7 +4292,7 @@ "zoomScale": 1.2, "clickEvents": [ { - "__id__": 111 + "__id__": 116 } ], "_N$interactable": true, @@ -4195,26 +4380,29 @@ }, "_children": [ { - "__id__": 114 + "__id__": 119 }, { - "__id__": 117 + "__id__": 122 }, { - "__id__": 120 + "__id__": 125 + }, + { + "__id__": 131 } ], "_active": true, "_components": [ { - "__id__": 126 + "__id__": 136 }, { - "__id__": 127 + "__id__": 137 } ], "_prefab": { - "__id__": 128 + "__id__": 138 }, "_opacity": 255, "_color": { @@ -4268,17 +4456,17 @@ "_name": "New Sprite", "_objFlags": 0, "_parent": { - "__id__": 113 + "__id__": 118 }, "_children": [], "_active": true, "_components": [ { - "__id__": 115 + "__id__": 120 } ], "_prefab": { - "__id__": 116 + "__id__": 121 }, "_opacity": 255, "_color": { @@ -4332,7 +4520,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 114 + "__id__": 119 }, "_enabled": true, "_materials": [ @@ -4375,17 +4563,17 @@ "_name": "add", "_objFlags": 0, "_parent": { - "__id__": 113 + "__id__": 118 }, "_children": [], "_active": true, "_components": [ { - "__id__": 118 + "__id__": 123 } ], "_prefab": { - "__id__": 119 + "__id__": 124 }, "_opacity": 255, "_color": { @@ -4439,7 +4627,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 117 + "__id__": 122 }, "_enabled": true, "_materials": [ @@ -4482,21 +4670,21 @@ "_name": "select", "_objFlags": 0, "_parent": { - "__id__": 113 + "__id__": 118 }, "_children": [ { - "__id__": 121 + "__id__": 126 } ], "_active": false, "_components": [ { - "__id__": 124 + "__id__": 129 } ], "_prefab": { - "__id__": 125 + "__id__": 130 }, "_opacity": 255, "_color": { @@ -4550,17 +4738,17 @@ "_name": "New Sprite", "_objFlags": 0, "_parent": { - "__id__": 120 + "__id__": 125 }, "_children": [], "_active": true, "_components": [ { - "__id__": 122 + "__id__": 127 } ], "_prefab": { - "__id__": 123 + "__id__": 128 }, "_opacity": 255, "_color": { @@ -4614,7 +4802,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 121 + "__id__": 126 }, "_enabled": true, "_materials": [ @@ -4657,7 +4845,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 120 + "__id__": 125 }, "_enabled": true, "_materials": [ @@ -4695,12 +4883,194 @@ "fileId": "cd60XPPnNLy6ISaU5JgQcu", "sync": false }, + { + "__type__": "cc.Node", + "_name": "remaining", + "_objFlags": 0, + "_parent": { + "__id__": 118 + }, + "_children": [ + { + "__id__": 132 + } + ], + "_active": false, + "_components": [ + { + "__id__": 134 + } + ], + "_prefab": { + "__id__": 135 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 68, + "height": 74 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 79.398, + -83.642, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "number", + "_objFlags": 0, + "_parent": { + "__id__": 131 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 133 + }, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 0 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "20JyKdHpFG1r00X+Kul+Qa", + "sync": false + }, { "__type__": "cc.Sprite", "_name": "", "_objFlags": 0, "node": { - "__id__": 113 + "__id__": 131 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "d45452ee-6ac4-4adc-ad49-cd876b55589f" + }, + "_type": 0, + "_sizeMode": 1, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "2a+W4C5TdABKuSIdXj1E49", + "sync": false + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 118 }, "_enabled": true, "_materials": [ @@ -4732,7 +5102,7 @@ "_name": "", "_objFlags": 0, "node": { - "__id__": 113 + "__id__": 118 }, "_enabled": true, "_normalMaterial": null, @@ -4793,7 +5163,7 @@ "hoverSprite": null, "_N$disabledSprite": null, "_N$target": { - "__id__": 113 + "__id__": 118 }, "_id": "" }, diff --git a/tools/test-start-boosters.cjs b/tools/test-start-boosters.cjs index ceed2a6a..45b1198c 100644 --- a/tools/test-start-boosters.cjs +++ b/tools/test-start-boosters.cjs @@ -194,7 +194,8 @@ test('simultaneous shot planning leaves the board untouched and all planned cuts function openingFixture(f=rocketFixture(5),random=()=>0.8,loadout={streak:3,hourglass:true,rocket:true}) { engine.Node.prototype.setScale=function(x,y=x){this.scaleX=x;this.scaleY=y;}; Object.defineProperty(engine.Node.prototype,'childrenCount',{get(){return this.children.length;},configurable:true}); - class Label{};class LabelOutline{};class Skeleton{constructor(){this.setAnimation=()=>({animation:{duration:0.35}});}} + // zhandan.json 的真实 eff 长 1.4 秒,避免短 Mock 漏掉爆炸被提前截断的问题。 + class Label{};class LabelOutline{};class Skeleton{constructor(){this.track={animation:{duration:1.4},trackTime:0};this.setAnimation=()=>this.track;}} class OpeningNode extends engine.Node { convertToNodeSpaceAR(p){return engine.v2(p.x-this.x,p.y-this.y);} } @@ -259,10 +260,10 @@ test('all seven props launch together with distinct trails, grant exactly 40 sec assert.equal(opening.update(30,true),false);assert.equal(events.length,0); listeners.hide();assert.equal(opening.update(30,false),false);assert.equal(events.length,0);listeners.show(); const boxScale=opening.box.scaleX,boxY=opening.box.y; - opening.update(0.4,false); + opening.update(0.25,false); assert.equal(opening.box.x,0);assert.equal(opening.box.y,boxY);assert.notEqual(opening.box.scaleX,boxScale); assert.ok(opening.carriedEntries.every(e=>e.node.active&&!e.arrived));assert.equal(events.length,0); - opening.update(0.4,false); + opening.update(0.3,false); assert.equal(opening.carriedEntries.length,2); assert.ok(opening.carriedEntries.every(e=>e.node.active&&e.trail&&!e.arrived)); assert.ok(opening.carriedEntries[0].from.x=1.4-track.trackTime,'the remaining real explosion must finish'); + } + assert.ok(flight.impactSeconds>=flight.trail.life+flight.trail.lifeVar,'the existing trail must finish'); }else if(!flight.shot)speeds.push(distance/step); }); assert.equal(f.map.timeNumber,beforeTime+10*newlyLanded); @@ -339,7 +346,7 @@ test('all seven props launch together with distinct trails, grant exactly 40 sec if(flying||effectsPending||digitsPending)assert.equal(done,false); if(!flying&&effectsPending)sawEffectsPending=true; if(digitsPending)sawDigitsPending=true; - if(opening.elapsed>=0.25)assert.equal(opening.launchCloud.active,false,'launch smoke clears promptly'); + if(opening.elapsed>=0.65)assert.equal(opening.launchCloud.active,false,'layered launch smoke finishes before opening completion'); if(newlyLanded&&!checkedPause){ const state=JSON.stringify([opening.elapsed,f.map.timeNumber,displayed, opening.flights.map(f=>[f.node.x,f.node.y,f.landed,f.impactElapsed])]); @@ -355,10 +362,10 @@ test('all seven props launch together with distinct trails, grant exactly 40 sec assert.equal(new Set(arrivalTimes.slice(0,hourglasses.length)).size,hourglasses.length); assert.ok(Math.max(...speeds)/Math.min(...speeds)<1.02,'hourglasses keep the same speed along every curve'); const averageSpeeds=travelled.map((distance,i)=>distance/arrivalTimes[i]); - for(const [first,last,expected] of [[0,hourglasses.length,1100],[hourglasses.length,opening.flights.length,700]]){ + for(const [first,last,expected] of [[0,hourglasses.length,1100],[hourglasses.length,opening.flights.length,950]]){ const group=averageSpeeds.slice(first,last); assert.ok(Math.max(...group)/Math.min(...group)<1.02,'each prop type keeps one travel speed'); - assert.ok(group.every(speed=>Math.abs(speed/expected-1)<0.02),'rocket slowdown must not change hourglass speed'); + assert.ok(group.every(speed=>Math.abs(speed/expected-1)<0.02),'rocket timing must not change hourglass speed'); } assert.equal(rising.size,3);assert.equal(falling.size,3); assert.equal(events.filter(event=>event==='time').length,4); @@ -376,7 +383,7 @@ test('all seven props launch together with distinct trails, grant exactly 40 sec hourglassPrefab:f.map.hourglassPrefab,rocketPrefab:f.map.rocketPrefab,rocketBoxPrefab:f.map.rocketBoxPrefab, SceneManager:f.map.SceneManager}); const entry=new Opening(next.map,{streak:3,...carried},()=>{}); - entry.update(0.9,false); + entry.update(0.65,false); assert.equal(entry.carriedEntries.length,Number(carried.hourglass)+Number(carried.rocket)); assert.equal(entry.carriedEntries.some(e=>!e.rocket),carried.hourglass); assert.equal(entry.carriedEntries.some(e=>e.rocket),carried.rocket); @@ -411,7 +418,7 @@ test('real streak tiers and carried selections determine counts, box frame and a assert.ok(Math.abs(node.height*node.scaleY-322*0.65)<1e-6,'rocket-only loadouts keep the same initial size'); assert.equal(node.scaleX,node.scaleY); } - opening.update(1.1,false); + opening.update(0.85,false); assert.deepEqual(usedCarried,[],'selection and entry do not consume inventory'); assert.equal(opening.launchCloud.children.length,Number(streak>0)+Number(hourglass)+Number(rocket)); opening.update(0.11,false); @@ -459,8 +466,8 @@ test('longer arcs preserve repeated square and protected-edge impacts even when assert.equal(new Set(flights.map(f=>f.shot.origin)).size,1); for(let i=1;i=previous+70-1e-6,'same-origin arrivals retain at least 0.1 seconds separation'); - assert.ok(flights[i].control.y>Math.max(flights[i].from.y,flights[i].to.y)+600, + assert.ok(current>=previous+95-1e-6,'same-origin arrivals retain at least 0.1 seconds separation'); + assert.ok(flights[i].control.y>Math.max(flights[i].from.y,flights[i].to.y)+480, 'only the otherwise-too-short later arc is extended'); } let done=false; @@ -488,7 +495,7 @@ test('different original blocks may land out of planning order and finish only a opening.update(1.21,false); const flights=opening.flights.filter(f=>f.shot); assert.ok(flights[1].distances.at(-1) { + const cache = { threeWinStreak: 1, threeWinStreakPending: false, threeWinStreakUnsynced: null }; + const f = fixture(cache); + f.cc.fx.GameConfig.forceThreeWinStreak = true; + f.Streak.restore(); + assert.equal(f.info.threeWinStreak, 3); + f.Streak.restore({ threeWinStreak: 0 }, true); + assert.equal(f.info.threeWinStreak, 3); + new f.Streak(true).fail(); + for (const result of ['fail', 'win', 'fail']) { + const round = new f.Streak(true); + round.begin(); + round[result](); + assert.equal(f.info.threeWinStreak, 3); + assert.deepEqual(f.storage, cache); + } + assert.equal(f.info.winStreak, 10); + assert.equal(f.saves.length, 0); + const reloaded = fixture(f.storage); + reloaded.cc.fx.GameConfig.forceThreeWinStreak = true; + reloaded.Streak.restore(); + reloaded.Streak.restore({ threeWinStreak: 0 }); + assert.equal(reloaded.info.threeWinStreak, 3); + + f.cc.fx.GameConfig.forceThreeWinStreak = false; + f.Streak.restore(); + assert.equal(f.info.threeWinStreak, 1); + const round = new f.Streak(true); + round.begin(); + round.fail(); + assert.equal(f.info.threeWinStreak, 0); + assert.equal(f.saves.length, 1); +}); + test('success grows through three tiers and stays capped; each round settles only once', () => { const f = fixture(); for (const expected of [1, 2, 3, 3, 3]) { diff --git a/tools/test-three-wins-flow.cjs b/tools/test-three-wins-flow.cjs index 334f9d08..a45de442 100644 --- a/tools/test-three-wins-flow.cjs +++ b/tools/test-three-wins-flow.cjs @@ -25,6 +25,56 @@ const output = { exports: {} }; vm.runInNewContext(compile(read('assets/Script/StartBoostLoadout.ts')), { module: output, exports: output.exports, Date }); const boosts = output.exports; +test('remaining badges track each inventory, selection and live depletion without repainting unchanged values', () => { + const prefab = JSON.parse(read('assets/prefab/pop/threeWins.prefab')); + function build(index) { + const source = prefab[index]; + return { name: source._name, active: source._active, + children: (source._children || []).map(child => build(child.__id__)), + getChildByName(name) { return this.children.find(child => child.name === name); } }; + } + const panel = { node: build(1), data: null, selected: { hourglass: false, rocket: false }, playEntry() {}, + renderNumber(node, value) { node.value = value; node.renders = (node.renders || 0) + 1; }, + clearNumber(node) { node.value = null; }, addCountSymbol(node, text) { node.value = text; } }; + for (const name of ['renderProp', 'refreshInventory', 'setSelected']) { + panel[name] = method('assets/Script/ThreeWinsPanel.ts', name); + } + const inventory = { hourglassAmount: 3, rocketAmount: 0, infiniteHourglass: false, infiniteRocket: false }; + const manager = { threeWinsPanel: panel, threeWinsData: () => ({ ...inventory }), node: { getChildByName: () => null } }; + const update = method('assets/Script/SceneManager.ts', 'update'); + const state = (prop, visible, count) => { + const node = panel.node.getChildByName(prop); + for (const name of ['add', 'select', 'remaining']) assert.equal(node.getChildByName(name).active, name === visible); + assert.equal(node.getChildByName('remaining').getChildByName('number').value, count); + assert.equal(node.getChildByName('amount'), undefined); + }; + update.call(manager, 0); + state('hourglass', 'remaining', 3); state('rocket', 'add', 0); + const number = panel.node.getChildByName('hourglass').getChildByName('remaining').getChildByName('number'); + update.call(manager, 0); + assert.equal(number.renders, 1, 'unchanged stock does not rebuild the digits every frame'); + panel.setSelected('hourglass', true); state('hourglass', 'select', 3); + panel.setSelected('hourglass', false); state('hourglass', 'remaining', 3); + panel.setSelected('rocket', true); state('rocket', 'add', 0); + assert.equal(panel.selected.rocket, false); + inventory.rocketAmount = 2; + update.call(manager, 0); state('rocket', 'remaining', 2); + panel.setSelected('rocket', true); state('rocket', 'select', 2); + inventory.hourglassAmount = 12; + update.call(manager, 0); state('hourglass', 'remaining', 12); state('rocket', 'select', 2); + inventory.rocketAmount = -1; + update.call(manager, 0); state('rocket', 'add', -1); + assert.equal(panel.selected.rocket, false, 'depletion clears stale selection'); + inventory.rocketAmount = 0; inventory.infiniteRocket = true; + update.call(manager, 0); state('rocket', 'remaining', '∞'); + panel.setSelected('rocket', true); state('rocket', 'select', '∞'); + inventory.infiniteRocket = false; + update.call(manager, 0); state('rocket', 'add', 0); + assert.equal(panel.selected.rocket, false, 'expiry restores the unavailable state'); + manager.threeWinsPanel = null; + update.call(manager, 0); +}); + test('GameScene references all opening prefabs so release builds include them', () => { const scene = JSON.parse(read('assets/Scene/GameScene.fire')); const map = scene.find(component => Object.hasOwn(component, 'hourglassPrefab'));