更新首页homeScene涉及到的按钮,以及涉及到的所有分包或者活动的按钮 增加震动

This commit is contained in:
COMPUTER\EDY 2026-09-22 17:38:43 +08:00
parent b028946207
commit 2e0d1826f1
12 changed files with 229 additions and 10 deletions

View File

@ -20,6 +20,7 @@ import { initProvinceLocator } from "./module/Position/GetPosition";
import LoadingCatAnimation from "./LoadingCatAnimation";
import BlockAssetManager from "./module/Tool/BlockAssetManager";
import MiniProgramBenefitsBridge from "./module/MiniProgramBenefitsBridge";
import { onHomeButtonTouchEnd, vibrateHomeButton } from "./module/Tool/HomeButtonVibration";
const { ccclass, property } = cc._decorator;
@ccclass
@ -46,6 +47,7 @@ export default class JiaZai extends cc.Component {
}
// 新的主动操作取代尚未打开的按需弹窗;资源下载仍正常完成并写入缓存。
this.homePopupOpenRequest++;
if (vibrateHomeButton(event)) return;
try {
cc.fx.GameTool.setVibrate("light", 1);
} catch (error) {
@ -366,6 +368,14 @@ export default class JiaZai extends cc.Component {
MiniGameSdk.API.showToast("当前设备内存不足,排行榜暂不可用");
}
onEnable() {
this.node.on(cc.Node.EventType.TOUCH_END, onHomeButtonTouchEnd, this, true);
}
onDisable() {
this.node.off(cc.Node.EventType.TOUCH_END, onHomeButtonTouchEnd, this, true);
}
onLoad() {
// 首页在进关前展示连胜;覆盖登录或缓存恢复的测试初始值。
if (cc.fx.GameConfig.forceOldRainbowWinStreakC || cc.fx.GameConfig.forceRainbowWinStreakB) {

View File

@ -1022,7 +1022,7 @@ export default class MapConroler extends cc.Component {
const hand = child.getChildByName('hand');
if (!hand) return;
const animation = hand.getComponent(GuideHand) || hand.addComponent(GuideHand);
animation.rippleMaterial = rippleSprite.sharedMaterials[0];
animation.rippleMaterial = rippleSprite.getMaterial(0);
});
this.propGuideMask = guide;
guide.parent = this.node.parent;

View File

@ -1,5 +1,6 @@
import LoadingCatAnimation from "../LoadingCatAnimation";
import SevenDayGiftBootstrapApi from "../seven_day_gift/SevenDayGiftBootstrapApi";
import { vibrateHomeButton } from "./Tool/HomeButtonVibration";
declare const wx: any;
@ -18,7 +19,7 @@ export default class MiniProgramBenefitsBridge {
private entryRed: cc.Node = null;
private entryFrame: cc.SpriteFrame = null;
private entryVisual: cc.Node = null;
private onEntryTouch = (): void => { this.open(); };
private onEntryTouch = (event: cc.Event): void => { vibrateHomeButton(event); this.open(); };
private session: any = null;
private rewardRequests = 0;
private testMount = 0;

View File

@ -0,0 +1,34 @@
const handledEvents = new WeakSet<cc.Event>();
/** 返回是否由首页统一处理;旧入口在其他场景仍可保留原有行为。 */
export function vibrateHomeButton(event?: cc.Event): boolean {
if (!event) return false;
const scene = cc.director.getScene();
if (!scene || scene.name !== "HomeScene") return false;
if (handledEvents.has(event)) return true;
handledEvents.add(event);
try {
cc.fx.GameTool.setVibrate("light", 1);
} catch (error) {
console.warn("HomeScene 按钮轻振动调用失败:", error);
}
return true;
}
/** 根节点捕获触摸,自动覆盖后来挂入首页的 Bundle 按钮。 */
export function onHomeButtonTouchEnd(event: cc.Event.EventTouch): void {
// 引擎可能复用事件对象,每次派发重新去重。
handledEvents.delete(event);
let target = event.target as cc.Node;
while (target) {
const button = target.getComponent(cc.Button);
if (button) {
if (button.interactable && button.enabledInHierarchy && (button as any)._pressed) {
vibrateHomeButton(event);
}
return;
}
if (target === event.currentTarget) return;
target = target.parent;
}
}

View File

@ -0,0 +1,10 @@
{
"ver": "1.1.0",
"uuid": "b9f1f6e3-d1e1-4a3d-a1a6-f3089fd502dd",
"importer": "typescript",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@ -1,4 +1,5 @@
import NumberToImage from "../../Script/NumberToImage";
import { vibrateHomeButton } from "../../Script/module/Tool/HomeButtonVibration";
import CareerList2 from "./CareerList2";
import CareerItem2 from "./CareerItem2";
@ -12,6 +13,7 @@ export default class CareerManager2 extends cc.Component {
if (!event) {
return;
}
if (vibrateHomeButton(event)) return;
try {
cc.fx.GameTool.setVibrate("light", 1);
} catch (error) {

View File

@ -1,3 +1,5 @@
import { vibrateHomeButton } from "../../Script/module/Tool/HomeButtonVibration";
const { ccclass, property } = cc._decorator;
type RewardType = "hammer" | "freeze" | "magic_wand" | "coin" | "infinite_health";
@ -59,10 +61,11 @@ export default class JungleOver extends cc.Component {
this.confirming = confirming;
}
private onClose(): void {
private onClose(event?: cc.Event): void {
if (this.confirming) {
return;
}
vibrateHomeButton(event);
this.confirming = true;
this.node.emit("jungle-over-close");
}

View File

@ -1,4 +1,5 @@
import MiniProgramBenefitsReward from "./MiniProgramBenefitsReward";
import { vibrateHomeButton } from "../../Script/module/Tool/HomeButtonVibration";
const { ccclass, property } = cc._decorator;
const TASKS = ["favorite_entry", "desktop_entry"];
@ -37,18 +38,22 @@ export default class MiniProgramBenefitsPanel extends cc.Component {
const key = index === 0 ? "list" : TASKS[index - 1];
const view = this.views[key] = cc.instantiate(prefab); view.parent = this.pages;
const content = view.getChildByName("Content") || view;
content.getChildByName("Close").on(cc.Node.EventType.TOUCH_END, () => {
content.getChildByName("Close").on(cc.Node.EventType.TOUCH_END, (event: cc.Event) => {
if (this.closing || this.busy) return;
vibrateHomeButton(event);
if (key === "list") this.closeWithAnimation();
else { this.page = "list"; this.message = ""; this.render(); }
});
if (key === "list") TASKS.forEach((task, i) => {
const row = view.getChildByName("Task" + i);
row.getChildByName("View").on(cc.Node.EventType.TOUCH_END, () => {
row.getChildByName("View").on(cc.Node.EventType.TOUCH_END, (event: cc.Event) => {
if (this.busy || this.closing) return;
vibrateHomeButton(event);
this.page = task; this.message = ""; this.render();
});
row.getChildByName("Claim").on(cc.Node.EventType.TOUCH_END, () => this.claim(task));
row.getChildByName("Claim").on(cc.Node.EventType.TOUCH_END, (event: cc.Event) => {
this.claim(task, event);
});
});
});
this.unsubscribe = host.subscribe(() => { if (!this.busy) this.render(); });
@ -155,8 +160,9 @@ export default class MiniProgramBenefitsPanel extends cc.Component {
cc.tween(overlay).delay(1.08).call(() => { ready = true; }).start();
});
}
private async claim(task: string): Promise<void> {
private async claim(task: string, event?: cc.Event): Promise<void> {
if (this.closing || this.busy || this.claiming) return;
vibrateHomeButton(event);
const token = this.generation;
this.claiming = true; this.busy = true; this.message = "正在领取…"; this.render();
try {

View File

@ -3,6 +3,7 @@ import SevenDayGiftRuntime from "./SevenDayGiftRuntime";
import ActivityPopupAnimator, { ActivityPopupAnimationType } from "../Script/ActivityPopupAnimator";
import RewardClaimEffect from "../reward_claim_effect/RewardClaimEffect";
import SevenDayHitTest from "./SevenDayHitTest";
import { vibrateHomeButton } from "../Script/module/Tool/HomeButtonVibration";
const { ccclass, property } = cc._decorator;
const TOTAL_DAYS = 7;
@ -170,7 +171,7 @@ export default class SevenDayGift extends cc.Component {
while (hit && hit !== this.panel) {
const index = this.cards.indexOf(hit);
if (index >= 0) {
this.onCardClicked(index + 1);
this.onCardClicked(index + 1, event);
return;
}
hit = hit.parent;
@ -234,7 +235,7 @@ export default class SevenDayGift extends cc.Component {
}
// 只允许领取当前可领天数,其他卡片点击无效。
private onCardClicked(day: number) {
private onCardClicked(day: number, event?: cc.Event) {
const reward = this.findReward(day);
if (reward && reward.claimed) {
return;
@ -244,6 +245,7 @@ export default class SevenDayGift extends cc.Component {
}
if (this.claiming || !this.claimHandler) return;
vibrateHomeButton(event);
this.requestCurrentReward();
}

View File

@ -0,0 +1,145 @@
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');
function evaluate(source, globals) {
return vm.runInNewContext(ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS },
}).outputText, globals);
}
function method(file, name, globals) {
const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
let found;
function visit(node) {
if (ts.isMethodDeclaration(node) && node.name.getText(source) === name) found = node;
else ts.forEachChild(node, visit);
}
visit(source);
assert.ok(found, `${file}: ${name}`);
return evaluate(`({ ${found.getText(source).replace(/^private\s+/, '')} }).${name};`, globals);
}
function fixture() {
const state = { scene: { name: 'HomeScene' }, pulses: [], warnings: [] };
const cc = { Button: 'Button', Node: { EventType: { TOUCH_END: 'touchend' } },
director: { getScene: () => state.scene }, fx: { GameConfig: { GM_INFO: { vibrateOpen: true } } } };
const wx = { vibrateShort: options => state.pulses.push(options.type) };
cc.fx.GameTool = { setVibrate: method('assets/Script/module/Tool/GameTool.ts', 'setVibrate', { cc, wx }) };
const globals = { cc, exports: {}, console: { warn: (...args) => state.warnings.push(args) } };
evaluate(fs.readFileSync('assets/Script/module/Tool/HomeButtonVibration.ts', 'utf8'), globals);
Object.assign(globals, globals.exports);
const node = (parent = null, button = null) => ({ parent, getComponent: () => button });
const root = node();
const button = { interactable: true, enabledInHierarchy: true, _pressed: true };
const target = node(root, button);
const event = { target, currentTarget: root };
return { state, cc, wx, globals, node, root, button, event, ...globals.exports };
}
test('existing and late-mounted bundle buttons vibrate once, including child hit targets', () => {
const f = fixture();
f.onHomeButtonTouchEnd(f.event);
f.vibrateHomeButton(f.event);
assert.deepEqual(f.state.pulses, ['light']);
const popup = f.node(f.root);
const dynamicButton = f.node(popup, { ...f.button });
f.onHomeButtonTouchEnd({ target: f.node(dynamicButton), currentTarget: f.root });
assert.deepEqual(f.state.pulses, ['light', 'light']);
});
test('disabled buttons, cancelled presses and empty backgrounds do not vibrate', () => {
for (const property of ['interactable', 'enabledInHierarchy', '_pressed']) {
const f = fixture(); f.button[property] = false;
f.onHomeButtonTouchEnd(f.event);
assert.equal(f.state.pulses.length, 0, property);
}
const f = fixture();
f.onHomeButtonTouchEnd({ target: f.node(f.root), currentTarget: f.root });
assert.equal(f.state.pulses.length, 0);
});
test('reused event objects vibrate on each new dispatch, but never twice per dispatch', () => {
const f = fixture();
for (let i = 0; i < 2; i++) {
f.onHomeButtonTouchEnd(f.event);
f.vibrateHomeButton(f.event);
assert.equal(f.state.pulses.length, i + 1);
}
});
test('respects the vibration switch and leaves GameScene and other scenes untouched', () => {
const f = fixture();
f.cc.fx.GameConfig.GM_INFO.vibrateOpen = false;
f.onHomeButtonTouchEnd(f.event);
assert.equal(f.state.pulses.length, 0);
f.cc.fx.GameConfig.GM_INFO.vibrateOpen = true;
for (const scene of [{ name: 'GameScene' }, { name: 'LoadScene' }, null]) {
f.state.scene = scene;
f.onHomeButtonTouchEnd(f.event);
assert.equal(f.vibrateHomeButton({ target: f.event.target }), false);
assert.equal(f.state.pulses.length, 0);
}
});
test('legacy Home and Career callbacks do not double vibrate or alter popup request cancellation', () => {
const f = fixture();
const homeClick = method('assets/Script/JiaZai.ts', 'vibrateButtonClick', f.globals);
const careerClick = method('assets/career/script/CareerManager2.ts', 'vibrateButtonClick', f.globals);
const home = { homePopupOpenRequest: 7 };
f.onHomeButtonTouchEnd(f.event);
homeClick.call(home, f.event);
careerClick(f.event);
assert.equal(home.homePopupOpenRequest, 8);
assert.deepEqual(f.state.pulses, ['light']);
homeClick.call(home);
assert.equal(home.homePopupOpenRequest, 8);
f.state.scene = { name: 'GameScene' };
careerClick({});
assert.deepEqual(f.state.pulses, ['light', 'light'], 'preserves the legacy callback outside Home');
});
test('vibration API errors do not interrupt click handling or cause a duplicate attempt', () => {
const f = fixture(); let attempts = 0;
f.wx.vibrateShort = () => { attempts++; throw Error('unavailable'); };
assert.doesNotThrow(() => f.onHomeButtonTouchEnd(f.event));
assert.doesNotThrow(() => f.vibrateHomeButton(f.event));
assert.equal(attempts, 1);
assert.equal(f.state.warnings.length, 1);
});
test('Home component registers capture on enable and removes the same listener on disable', () => {
const f = fixture(); const calls = [];
const home = { node: { on: (...args) => calls.push(['on', ...args]), off: (...args) => calls.push(['off', ...args]) } };
method('assets/Script/JiaZai.ts', 'onEnable', f.globals).call(home);
method('assets/Script/JiaZai.ts', 'onDisable', f.globals).call(home);
assert.equal(calls[0][0], 'on'); assert.equal(calls[1][0], 'off');
assert.deepEqual(calls[0].slice(1), ['touchend', f.onHomeButtonTouchEnd, home, true]);
assert.deepEqual(calls[0].slice(1), calls[1].slice(1));
});
test('touch-only Jungle close vibrates once and preserves confirmation guarding', () => {
const f = fixture(); let closes = 0;
const close = method('assets/jungle_treasure/script/JungleOver.ts', 'onClose', f.globals);
const panel = { confirming: false, node: { emit: () => closes++ } };
const event = { target: f.node(f.root), currentTarget: f.root };
f.onHomeButtonTouchEnd(event);
assert.equal(f.state.pulses.length, 0);
close.call(panel, event); close.call(panel, event);
assert.equal(closes, 1); assert.deepEqual(f.state.pulses, ['light']);
});
test('seven-day cards only vibrate when the reward can be claimed', () => {
const f = fixture(); let claims = 0;
const click = method('assets/seven_day_gift/SevenDayGift.ts', 'onCardClicked', f.globals);
const panel = { findReward: () => ({ claimed: false }), info: { canClaim: true, nextRewardDay: 2 },
claiming: false, claimHandler() {}, requestCurrentReward: () => claims++ };
click.call(panel, 1, {});
assert.equal(f.state.pulses.length, 0);
click.call(panel, 2, {});
assert.equal(claims, 1); assert.deepEqual(f.state.pulses, ['light']);
panel.claiming = true; click.call(panel, 2, {});
assert.equal(claims, 1); assert.equal(f.state.pulses.length, 1);
});

View File

@ -73,7 +73,9 @@ function inflate(data, id) {
for (const ref of raw._components || []) {
const component = data[ref.__id__];
node.components[component.__type__] = { ...component, updateAlignment() {}, setState() {} };
if (component.__type__ === 'cc.Sprite') node.components[component.__type__].sharedMaterials = component._materials;
if (component.__type__ === 'cc.Sprite') {
node.components[component.__type__].getMaterial = index => component._materials[index] || null;
}
}
for (const ref of raw._children || []) inflate(data, ref.__id__).parent = node;
return node;
@ -144,6 +146,9 @@ for (const [level, name, amount] of [[8, 'destroyBtn', 'hammerAmount'], [11, 'ti
map.showPropGuideMask(level);
assert.equal(map.propGuideMask, guide);
const activeButton = buttons.find(button => button.active);
const rippleSprite = gameNode.getChildByName('Bottom').getChildByName('rippleShrink').getComponent('cc.Sprite');
assert.ok(rippleSprite.getMaterial(0));
assert.equal(activeButton.getChildByName('hand').getComponent('GuideHand').rippleMaterial, rippleSprite.getMaterial(0));
assert.equal(activeButton.getComponent('cc.Button').clickEvents[0].customEventData, 'guide');
activeButton.click();
assert.equal(map.propGuideMask, null);

View File

@ -60,6 +60,7 @@ function environment() {
if(name==='../LoadingCatAnimation')return {default:{play:node=>{node.active=true;return true},stop:node=>{node.active=false}}};
if(name==='./MiniProgramBenefitsReward')return {default:class {request(){throw Error('Unexpected network adapter call in UI test');}}};
if(name.endsWith('/MiniProgramBenefitsBridge'))return {default:Bridge};
if(name.endsWith('/HomeButtonVibration'))return {vibrateHomeButton() {}};
throw new Error('Unexpected dependency: '+name);
},cc,wx,window:{wx},console,setTimeout,clearTimeout,Promise,Set},{filename:file});
return module.exports.default;