535 lines
37 KiB
JavaScript
535 lines
37 KiB
JavaScript
const { installCountFrames, countText } = require('./daily-count-test-utils.cjs');
|
||
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const vm = require('node:vm');
|
||
const { stripTypeScriptTypes } = require('node:module');
|
||
const root = path.resolve(__dirname, '..');
|
||
const prefab = JSON.parse(fs.readFileSync(path.join(root, 'assets/action_bundle/prefab/dailyQuests.prefab'), 'utf8'));
|
||
const source = fs.readFileSync(path.join(root, 'assets/action_bundle/script/DailyQuests.ts'), 'utf8')
|
||
.replace(/^import .*;\r?$/gm, '').replace(/^\s*@(?:ccclass|property[^\r\n]*)\r?$/gm, '')
|
||
.replace('export default class DailyQuests', 'class DailyQuests');
|
||
const trackerSource = fs.readFileSync(path.join(root, 'assets/Script/module/Task/DailyTaskProgressTracker.ts'), 'utf8')
|
||
.replace(/^export type /gm, 'type ').replace('export default class', 'class');
|
||
const code = stripTypeScriptTypes(trackerSource + '\n' + source) + '\nglobalThis.DailyQuests = DailyQuests;';
|
||
class Node {
|
||
constructor(o) {
|
||
if (typeof o === 'string') o = { _name: o, _active: true, _contentSize: { width: 0, height: 0 }, _trs: { array: [0, 0, 0, 0, 0, 0, 1, 1, 1] }, _eulerAngles: { z: 0 }, _anchorPoint: { x: 0.5, y: 0.5 } };
|
||
this.name = o._name; this.active = o._active; this.width = o._contentSize.width; this.height = o._contentSize.height;
|
||
this.x = o._trs.array[0]; this.y = o._trs.array[1]; this.scaleX = o._trs.array[7]; this.scaleY = o._trs.array[8];
|
||
this.angle = o._eulerAngles.z; this.anchorX = o._anchorPoint.x; this.anchorY = o._anchorPoint.y;
|
||
this.children = []; this.components = {};
|
||
}
|
||
set scale(value) { this.scaleX = value; this.scaleY = value; }
|
||
get scale() { return this.scaleX; }
|
||
get activeInHierarchy() { return !this.destroyed && this.active && (!this.parent || this.parent.activeInHierarchy); }
|
||
on(type, handler, target, capture) { (this.listeners ||= []).push({ type, handler, target, capture }); }
|
||
off(type, handler, target, capture) { this.listeners = this.listeners.filter(item => item.handler !== handler || item.capture !== capture); }
|
||
setContentSize(width, height) { this.width = width; this.height = height; }
|
||
getBoundingBoxToWorld() {
|
||
let x = this.x, y = this.y, parent = this.parent;
|
||
while (parent) { x = x * parent.scaleX + parent.x; y = y * (parent.scaleY || 1) + parent.y; parent = parent.parent; }
|
||
return { contains: point => Math.abs(point.x - x) <= this.width * this.scaleX / 2 && Math.abs(point.y - y) <= this.height * (this.scaleY || 1) / 2 };
|
||
}
|
||
getChildByName(name) { return this.children.find(node => node.name === name); }
|
||
addComponent(type) { return this.components[String(type)] = {}; }
|
||
removeFromParent() { if (this.parent) this.parent.children = this.parent.children.filter(n => n !== this); this.parent = null; }
|
||
destroy() { this.destroyed = true; this.removeFromParent(); }
|
||
getComponent(type) { return this.components[type] || null; }
|
||
addChild(node) { assert.ok(node, 'addChild must receive a node'); node.parent = this; this.children.push(node); }
|
||
setSiblingIndex() { }
|
||
setPosition(x, y) { this.x = x; this.y = y; }
|
||
convertToWorldSpaceAR(point) {
|
||
const result = { x: point.x * this.scaleX + this.x, y: point.y * this.scaleY + this.y };
|
||
return this.parent ? this.parent.convertToWorldSpaceAR(result) : result;
|
||
}
|
||
convertToNodeSpaceAR(point) {
|
||
const local = this.parent ? this.parent.convertToNodeSpaceAR(point) : point;
|
||
return { x: (local.x - this.x) / this.scaleX, y: (local.y - this.y) / this.scaleY };
|
||
}
|
||
}
|
||
function build(id) {
|
||
const o = prefab[id], node = new Node(o);
|
||
for (const ref of o._components) {
|
||
const component = structuredClone(prefab[ref.__id__]);
|
||
if (component.clickEvents) component.clickEvents = component.clickEvents.map(event => event.__id__ == null ? event : structuredClone(prefab[event.__id__]));
|
||
node.components[component.__type__] = component;
|
||
}
|
||
if (node.components['cc.Layout']) node.components['cc.Layout'].updateLayout = () => {
|
||
const active = node.children.filter(child => child.active);
|
||
node.width = active.reduce((sum, child) => sum + child.width * child.scaleX, 0) + Math.max(0, active.length - 1) * (node.components['cc.Layout'].spacingX ?? node.components['cc.Layout']['_N$spacingX']);
|
||
};
|
||
node.children = o._children.map(ref => build(ref.__id__)); node.children.forEach(child => child.parent = node); return node;
|
||
}
|
||
function clone(node) {
|
||
const copy = Object.assign(Object.create(Node.prototype), node);
|
||
copy.components = Object.fromEntries(Object.entries(node.components).map(([key, value]) => [key, { ...value }])); copy.children = node.children.map(clone); return copy;
|
||
}
|
||
function setup(stars = 15) {
|
||
const requests = [], toasts = [], tweens = [];
|
||
// Advance animation time deterministically, including the gap between loops.
|
||
// Only started tweens run; stopping one prevents any later property changes.
|
||
class Tween {
|
||
constructor(target) { this.target = target; this.steps = []; this.index = 0; this.elapsed = 0; this.cancelled = false; }
|
||
to(duration, properties, options = {}) { this.steps.push({ duration, properties, options }); return this; }
|
||
delay(duration) { this.steps.push({ duration }); return this; }
|
||
repeatForever(tween) { this.steps.push(...tween.steps); this.repeating = true; return this; }
|
||
start() { tweens.push(this); return this; }
|
||
stop() { this.cancelled = true; return this; }
|
||
advance(seconds) {
|
||
while (seconds > 1e-9 && !this.cancelled && this.index < this.steps.length) {
|
||
const step = this.steps[this.index];
|
||
this.initial ||= Object.fromEntries(Object.keys(step.properties || {}).map(key => [key, this.target[key]]));
|
||
const consumed = Math.min(seconds, step.duration - this.elapsed);
|
||
this.elapsed += consumed; seconds -= consumed;
|
||
const progress = Math.min(1, this.elapsed / step.duration);
|
||
const eased = step.options?.easing === 'sineInOut' ? (1 - Math.cos(Math.PI * progress)) / 2 : progress;
|
||
for (const [key, end] of Object.entries(step.properties || {})) this.target[key] = this.initial[key] + (end - this.initial[key]) * eased;
|
||
if (this.elapsed + 1e-9 < step.duration) return;
|
||
if (step.properties) Object.assign(this.target, step.properties);
|
||
this.index++; this.elapsed = 0; this.initial = null;
|
||
if (this.repeating && this.index === this.steps.length) this.index = 0;
|
||
}
|
||
}
|
||
static stopAllByTarget(target) { tweens.filter(tween => tween.target === target).forEach(tween => tween.stop()); }
|
||
}
|
||
const cc = {
|
||
Component: class { }, Node, Tween, tween: target => new Tween(target), Label: { toString: () => 'cc.Label', Overflow: { SHRINK: 2 }, HorizontalAlign: { CENTER: 1 }, VerticalAlign: { CENTER: 1 } }, RichText: 'cc.RichText', LabelOutline: 'cc.LabelOutline', Sprite: { toString: () => 'cc.Sprite', Type: { SLICED: 1 }, SizeMode: { CUSTOM: 0, TRIMMED: 1 } },
|
||
ProgressBar: 'cc.ProgressBar', Button: 'cc.Button', Layout: 'cc.Layout', _decorator: {}, Camera: { findCamera: () => null },
|
||
isValid: node => !!node && !node.destroyed, instantiate: clone, color: (r, g, b) => ({ r, g, b }), error: message => { throw Error(message); },
|
||
fx: { GameConfig: { GM_INFO: {} } }
|
||
};
|
||
cc.Node.EventType = { TOUCH_START: 'touchstart' };
|
||
cc.find = () => null;
|
||
cc.v2 = (x = 0, y = 0) => ({ x, y });
|
||
const context = {
|
||
cc, Date, console, setTimeout, Utils: {
|
||
getDailyQuestReward: (id, callback, scope) => requests.push([id, response => {
|
||
if (response.code === 1) cc.fx.GameConfig.GM_INFO.tasks = response.data.task;
|
||
callback(response);
|
||
}, scope])
|
||
},
|
||
MiniGameSdk: { API: { showToast: text => toasts.push(text) } }
|
||
};
|
||
cc.fx.GameTool = { getDailyTaskServerTime: () => Date.now(), claimDailyQuestReward: context.Utils.getDailyQuestReward };
|
||
cc.fx.GameTool.getCachedDailyQuests = () => null;
|
||
vm.runInNewContext(code, context);
|
||
const daily = new context.DailyQuests(); installCountFrames(daily); daily.node = build(1);
|
||
daily.content = daily.node.getChildByName('bg').getChildByName('content'); daily.touchStart = true;
|
||
daily.rewardIcons = ['xinCoin', 'ice', 'hammer', 'magic', 'skyLine'].map(name => {
|
||
const meta = JSON.parse(fs.readFileSync(path.join(root, `assets/res/public/${name}.png.meta`), 'utf8')).subMetas[name];
|
||
return { getRect: () => ({ width: meta.width, height: meta.height }) };
|
||
});
|
||
daily.openLoad = () => { }; daily.closeLoad = () => { };
|
||
// Reward presentation phases are covered by test-daily-reward-animation.cjs.
|
||
daily.playRewardAnimation = (rewards, complete) => complete();
|
||
daily.scheduleOnce = callback => { daily.timeout = callback; };
|
||
daily.unschedule = callback => { if (daily.timeout === callback) daily.timeout = null; };
|
||
const data = {
|
||
version: 2, weeklyStars: stars, dailyTasks: [], weeklyRewards: [35, 70, 105, 140].map(target => ({
|
||
rewardId: `weekly_${target}`, target, state: stars >= target ? 1 : 0,
|
||
rewards: target === 140 ? [{ type: 'infinite_rocket', count: 900 }, { type: 'infinite_hourglass', count: 900 }, { type: 'coin', count: 1500 }]
|
||
: [{ type: 'hammer', count: 1 }, { type: 'infinite_health', count: 900 }]
|
||
}))
|
||
};
|
||
cc.fx.GameConfig.GM_INFO.tasks = data;
|
||
const weekly = daily.node.getChildByName('bg').getChildByName('weeklyRewards');
|
||
const preview = weekly.getChildByName('preview');
|
||
daily.onLoad(); daily.renderWeeklyRewards(data);
|
||
return {
|
||
daily, data, weekly, preview, requests, toasts, cc, tweens,
|
||
advance(seconds) { tweens.forEach(tween => tween.advance(seconds)); }
|
||
};
|
||
}
|
||
|
||
test('weekly chests preserve the manually adjusted positions and top pivots', () => {
|
||
const { weekly } = setup(0), sizes = [78, 90, 100, 110], tops = [86, 94, 107, 65];
|
||
weekly.getChildByName('tiers').children.forEach((tier, index) => {
|
||
const chest = tier.getChildByName('chest');
|
||
assert.equal(chest.anchorX, 0.5); assert.equal(chest.anchorY, 1); assert.equal(chest.angle, 0);
|
||
assert.equal(chest.width, sizes[index]); assert.equal(chest.height, sizes[index]);
|
||
assert.equal(chest.scaleX, 1); assert.equal(chest.scaleY, 1);
|
||
// Keep the editor-adjusted positions rather than normalizing every chest height.
|
||
const left = chest.x - chest.anchorX * chest.width * chest.scaleX;
|
||
const bottom = chest.y - chest.anchorY * chest.height * chest.scaleY;
|
||
assert.equal(left, -sizes[index] / 2); assert.equal(bottom, tops[index] - sizes[index]);
|
||
assert.equal(bottom + chest.height * chest.scaleY, tops[index]);
|
||
});
|
||
});
|
||
|
||
test('only ready chest icons swing gently left and right, with two seconds at rest between loops', () => {
|
||
const h = setup(0), tiers = h.weekly.getChildByName('tiers').children;
|
||
h.data.weeklyRewards.forEach((reward, index) => reward.state = [0, 1, 2, 1][index]);
|
||
h.daily.renderWeeklyRewards(h.data);
|
||
const chests = tiers.map(tier => tier.getChildByName('chest'));
|
||
const stationary = tiers.flatMap(tier => [tier, tier.getChildByName('target'), tier.getChildByName('claimable'), tier.getChildByName('claimed')]);
|
||
const transforms = stationary.map(node => [node.x, node.y, node.angle, node.scaleX, node.scaleY]);
|
||
assert.deepEqual(h.tweens.map(tween => tween.target), [chests[1], chests[3]]);
|
||
for (let loop = 0; loop < 3; loop++) {
|
||
h.advance(0.06); assert.ok(chests[1].angle > 0 && chests[1].angle < 6);
|
||
h.advance(0.06); assert.equal(chests[1].angle, 6);
|
||
h.advance(0.24); assert.equal(chests[1].angle, -6);
|
||
h.advance(0.12); assert.equal(chests[1].angle, 0);
|
||
h.advance(1); assert.equal(chests[1].angle, 0);
|
||
h.advance(0.99); assert.equal(chests[1].angle, 0);
|
||
h.advance(0.01); assert.equal(chests[1].angle, 0);
|
||
assert.equal(chests[0].angle, 0); assert.equal(chests[2].angle, 0);
|
||
assert.deepEqual(stationary.map(node => [node.x, node.y, node.angle, node.scaleX, node.scaleY]), transforms);
|
||
}
|
||
h.advance(0.12); assert.equal(chests[1].angle, 6); assert.equal(chests[3].angle, 6);
|
||
assert.equal(h.tweens.length, 2, 'loops reuse the original tweens');
|
||
assert.deepEqual(h.data.weeklyRewards.map(reward => reward.state), [0, 1, 2, 1]);
|
||
});
|
||
|
||
test('repeated rendering keeps the current swing and pause instead of restarting or stacking tweens', () => {
|
||
const h = setup(35), chest = h.weekly.getChildByName('tiers').children[0].getChildByName('chest');
|
||
const tween = h.tweens[0];
|
||
h.advance(0.06); const angle = chest.angle;
|
||
for (let i = 0; i < 3; i++) h.daily.renderWeeklyRewards(h.data);
|
||
assert.equal(h.tweens.length, 1); assert.equal(h.tweens[0], tween); assert.equal(chest.angle, angle);
|
||
h.advance(0.06); assert.equal(chest.angle, 6);
|
||
h.advance(0.36); assert.equal(chest.angle, 0);
|
||
h.advance(1); h.daily.renderWeeklyRewards(h.data);
|
||
h.advance(0.99); assert.equal(chest.angle, 0);
|
||
h.advance(0.07); assert.ok(chest.angle > 0 && chest.angle < 6);
|
||
assert.equal(h.tweens.length, 1);
|
||
});
|
||
|
||
for (const state of [0, 2]) test(`changing a ready reward to state ${state} stops its swing and restores the authored angle`, () => {
|
||
const h = setup(0), chest = h.weekly.getChildByName('tiers').children[0].getChildByName('chest');
|
||
chest.angle = 11; h.data.weeklyRewards[0].state = 1; h.daily.renderWeeklyRewards(h.data);
|
||
h.advance(0.12); assert.equal(chest.angle, 17);
|
||
h.data.weeklyRewards[0].state = state; h.daily.renderWeeklyRewards(h.data);
|
||
assert.equal(chest.angle, 11); assert.equal(h.tweens[0].cancelled, true);
|
||
h.advance(10); assert.equal(chest.angle, 11);
|
||
assert.equal(h.daily.weeklyChestAnimations.length, 0);
|
||
h.data.weeklyRewards[0].state = 1; h.daily.renderWeeklyRewards(h.data);
|
||
h.advance(0.12); assert.equal(chest.angle, 17); assert.equal(h.tweens.length, 2);
|
||
});
|
||
|
||
test('removing one reward stops only its icon, and empty weekly data stops the remaining loops', () => {
|
||
const h = setup(140), tiers = h.weekly.getChildByName('tiers').children;
|
||
const chests = tiers.map(tier => tier.getChildByName('chest'));
|
||
h.advance(0.12); h.data.weeklyRewards.shift(); h.daily.renderWeeklyRewards(h.data);
|
||
assert.equal(tiers[0].active, false); assert.equal(chests[0].angle, 0); assert.equal(h.tweens[0].cancelled, true);
|
||
assert.equal(h.daily.weeklyChestAnimations.length, 3);
|
||
h.advance(0.24); assert.equal(chests[0].angle, 0); assert.equal(chests[1].angle, -6);
|
||
h.data.weeklyRewards = []; h.daily.renderWeeklyRewards(h.data);
|
||
assert.equal(h.weekly.active, false); assert.equal(h.daily.weeklyChestAnimations.length, 0);
|
||
assert.ok(h.tweens.every(tween => tween.cancelled));
|
||
h.advance(10); assert.ok(chests.every(chest => chest.angle === 0));
|
||
});
|
||
|
||
test('a removed weekly section releases all running chest animations', () => {
|
||
const h = setup(140), chests = h.weekly.getChildByName('tiers').children.map(tier => tier.getChildByName('chest'));
|
||
h.advance(0.12);
|
||
h.weekly.parent.children = h.weekly.parent.children.filter(node => node !== h.weekly);
|
||
h.daily.renderWeeklyRewards(h.data);
|
||
assert.ok(h.tweens.every(tween => tween.cancelled)); assert.equal(h.daily.weeklyChestAnimations.length, 0);
|
||
h.advance(10); assert.ok(chests.every(chest => chest.angle === 0));
|
||
});
|
||
|
||
test('disabling a panel restores its icons, inactive rendering stays stopped, and reopening starts fresh loops', () => {
|
||
const h = setup(140), chests = h.weekly.getChildByName('tiers').children.map(tier => tier.getChildByName('chest'));
|
||
h.advance(0.12); h.daily.node.active = false; h.daily.onDisable(); h.daily.onDisable();
|
||
assert.ok(h.tweens.every(tween => tween.cancelled)); assert.equal(h.daily.weeklyChestAnimations.length, 0);
|
||
h.daily.renderWeeklyRewards(h.data); h.advance(10);
|
||
assert.equal(h.tweens.length, 4); assert.ok(chests.every(chest => chest.angle === 0));
|
||
h.daily.node.active = true; h.daily.renderWeeklyRewards(h.data); h.daily.renderWeeklyRewards(h.data);
|
||
assert.equal(h.tweens.length, 8); assert.equal(h.daily.weeklyChestAnimations.length, 4);
|
||
h.advance(0.12); assert.ok(chests.every(chest => chest.angle === 6));
|
||
});
|
||
|
||
test('destroying a panel stops its loops without cancelling unrelated tweens on the same icon', () => {
|
||
const h = setup(35), chest = h.weekly.getChildByName('tiers').children[0].getChildByName('chest');
|
||
const unrelated = h.cc.tween(chest).to(1, { opacity: 100 }).start();
|
||
h.advance(0.12); h.daily.onDestroy(); h.daily.onDestroy();
|
||
assert.equal(h.tweens[0].cancelled, true); assert.equal(unrelated.cancelled, false);
|
||
assert.equal(h.daily.weeklyChestAnimations.length, 0); assert.equal(chest.angle, 0);
|
||
h.advance(1); assert.equal(chest.angle, 0); assert.equal(chest.opacity, 100);
|
||
});
|
||
test('weekly prefab references, nine-slice background and check assets are valid', () => {
|
||
function walk(value) {
|
||
if (!value || typeof value !== 'object') return;
|
||
if ('__id__' in value) assert.ok(prefab[value.__id__]);
|
||
Object.values(value).forEach(walk);
|
||
}
|
||
walk(prefab);
|
||
const { weekly, preview } = setup();
|
||
assert.equal(preview.getComponent('cc.Sprite')._type, 1);
|
||
const pop = JSON.parse(fs.readFileSync(path.join(root, 'assets/action_bundle/img/dailyQuest/pop.png.meta'), 'utf8'));
|
||
for (const key of ['borderLeft', 'borderRight', 'borderTop', 'borderBottom']) assert.equal(pop.subMetas.pop[key], 28);
|
||
assert.equal(preview.getChildByName('close'), undefined);
|
||
assert.equal(preview.getChildByName('btnClaim'), undefined);
|
||
const meta = JSON.parse(fs.readFileSync(path.join(root, 'assets/action_bundle/img/dailyQuest/dagou.png.meta'), 'utf8'));
|
||
for (const tier of weekly.getChildByName('tiers').children) {
|
||
const check = tier.getChildByName('claimed'); assert.ok(check.width > 0 && check.width <= tier.getChildByName('chest').width); assert.ok(check.height > 0 && check.height <= tier.getChildByName('chest').height);
|
||
assert.equal(check.getComponent('cc.Sprite')._spriteFrame.__uuid__, meta.subMetas.dagou.uuid);
|
||
assert.equal(tier.getComponent('cc.Button').clickEvents[0].handler, 'selectWeeklyReward');
|
||
}
|
||
});
|
||
test('weekly targets use weekly digit sprites in the prefab and on refresh', () => {
|
||
const { daily, weekly, data } = setup(15);
|
||
const component = prefab.find(value => value.weeklyDigits);
|
||
assert.deepEqual(component.weeklyDigits.map(ref => ref.__uuid__), daily.weeklyDigits.map(frame => frame.uuid));
|
||
const targets = prefab.filter(value => value._name === 'target');
|
||
assert.equal(targets.length, 4);
|
||
targets.forEach((target, index) => {
|
||
assert.equal(target._components.length, 0);
|
||
const text = String(data.weeklyRewards[index].target);
|
||
assert.equal(target._children.length, text.length);
|
||
target._children.forEach((ref, position) => {
|
||
const glyph = prefab[ref.__id__];
|
||
const sprite = prefab[glyph._components[0].__id__];
|
||
assert.equal(sprite.__type__, 'cc.Sprite');
|
||
assert.equal(sprite._spriteFrame.__uuid__, daily.weeklyDigits[Number(text[position])].uuid);
|
||
});
|
||
});
|
||
data.weeklyRewards.forEach((reward, index) => reward.target = [0, 68, 279, 1345][index]);
|
||
daily.renderWeeklyRewards(data);
|
||
weekly.getChildByName('tiers').children.forEach((tier, index) => {
|
||
const target = tier.getChildByName('target');
|
||
assert.equal(countText(target), String(data.weeklyRewards[index].target));
|
||
const group = target.getChildByName('RewardCountGlyphs');
|
||
group.children.filter(glyph => glyph.active).forEach(glyph => {
|
||
assert.ok(daily.weeklyDigits.includes(glyph.getComponent('cc.Sprite').spriteFrame));
|
||
assert.ok(glyph.height * glyph.scale * group.scale <= 34);
|
||
assert.ok((Math.abs(glyph.x) + glyph.width * glyph.scale / 2) * group.scale <= target.width / 2 + 1e-6);
|
||
});
|
||
});
|
||
});
|
||
test('weekly progress and total do not create rows in the daily list', () => {
|
||
const { daily, weekly, preview } = setup(15);
|
||
assert.equal(weekly.getChildByName('progress').getComponent('cc.ProgressBar').progress, 15 / 140);
|
||
assert.equal(countText(weekly.getChildByName('current').getChildByName('count')), '15');
|
||
assert.equal(daily.content.children.length, 1); assert.equal(preview.active, false);
|
||
});
|
||
|
||
test('all count containers use the configured number artwork and have no Label components', () => {
|
||
const { daily } = setup();
|
||
const component = prefab.find(value => value.rewardDigits);
|
||
assert.deepEqual(component.rewardDigits.map(ref => ref.__uuid__), daily.rewardDigits.map(frame => frame.uuid));
|
||
for (const node of prefab.filter(value => value.__type__ === 'cc.Node' && value._name === 'count')) {
|
||
assert.ok(node._components.every(ref => !['cc.Label', 'cc.LabelOutline'].includes(prefab[ref.__id__].__type__)));
|
||
assert.equal(node._children.length, 1, 'editor preview contains an image digit');
|
||
const glyph = prefab[node._children[0].__id__];
|
||
assert.equal(prefab[glyph._components[0].__id__].__type__, 'cc.Sprite');
|
||
}
|
||
});
|
||
|
||
test('image counts cover every digit, quantities and time units and fit their container when values change', () => {
|
||
const { daily, weekly } = setup();
|
||
const count = weekly.getChildByName('current').getChildByName('count');
|
||
for (const text of ['0123456789', '0', '×12', '1.5分钟', '+2000', '+60分钟', '140']) {
|
||
daily.setRewardCount(count, text);
|
||
assert.equal(countText(count), text);
|
||
let right = -count.width / 2;
|
||
const group = count.getChildByName('RewardCountGlyphs');
|
||
for (const glyph of group.children.filter(glyph => glyph.active)) {
|
||
const left = (glyph.x - glyph.width * glyph.scaleX / 2) * group.scale;
|
||
assert.ok(left >= right - 1e-8, 'glyphs do not overlap');
|
||
right = (glyph.x + glyph.width * glyph.scaleX / 2) * group.scale;
|
||
assert.ok(right <= count.width / 2 + 1e-8, 'long values stay within the amount slot');
|
||
assert.ok(glyph.height * glyph.scaleY * group.scale <= count.height);
|
||
}
|
||
const original = group.children.slice();
|
||
daily.setRewardCount(count, text);
|
||
assert.deepEqual(group.children, original, 'unchanged quantities reuse their glyphs');
|
||
}
|
||
});
|
||
test('clicking an unreached chest only shows its rewards', () => {
|
||
const { daily, weekly, preview, requests } = setup(15); daily.selectWeeklyReward(null, 'weekly_35');
|
||
assert.equal(preview.active, true); assert.equal(weekly.getChildByName('previewDismiss'), undefined);
|
||
const items = preview.getChildByName('rewards').children.filter(node => node.active);
|
||
assert.equal(items.length, 2); assert.ok(items.every(node => !node.getChildByName('stars').active));
|
||
assert.equal(countText(items[1].getChildByName('count')), '15分钟');
|
||
assert.equal(requests.length, 0);
|
||
});
|
||
test('clicking a ready chest claims immediately and adds the check only after success', () => {
|
||
const { daily, data, weekly, preview, requests, cc, tweens, advance } = setup(105);
|
||
const tier = weekly.getChildByName('tiers').getChildByName('weekly_105');
|
||
const check = tier.getChildByName('claimed'), chest = tier.getChildByName('chest');
|
||
const click = tier.getComponent('cc.Button').clickEvents[0];
|
||
advance(0.12); assert.equal(chest.angle, 6); assert.equal(tier.angle, 0); assert.equal(check.angle, 0);
|
||
let granted = 0;
|
||
daily.openReward = (id, rewards) => { assert.equal(id, 'weekly_105'); assert.equal(rewards.length, 2); granted++; };
|
||
daily[click.handler](null, click.customEventData); daily[click.handler](null, click.customEventData);
|
||
assert.equal(requests.length, 1); assert.equal(requests[0][0], 'weekly_105'); assert.equal(requests[0][2], 'weekly');
|
||
assert.equal(preview.active, false); assert.equal(check.active, false);
|
||
const result = structuredClone(data); result.weeklyRewards[2].state = 2;
|
||
requests[0][1]({ code: 1, data: { rewards: result.weeklyRewards[2].rewards, task: result } });
|
||
assert.equal(granted, 1); assert.equal(check.active, true); assert.equal(daily.touchStart, true);
|
||
assert.equal(chest.angle, 0); assert.equal(tweens.find(tween => tween.target === chest).cancelled, true);
|
||
advance(2.6); assert.equal(chest.angle, 0); assert.equal(check.angle, 0);
|
||
assert.equal(daily.weeklyChestAnimations.length, 2, 'other claimable rewards keep swinging');
|
||
daily.selectWeeklyReward(null, 'weekly_105'); assert.equal(requests.length, 1); assert.equal(preview.active, true);
|
||
});
|
||
test('clicking an already claimed chest only previews rewards', () => {
|
||
const { daily, data, weekly, preview, requests } = setup(140); data.weeklyRewards[3].state = 2; daily.renderWeeklyRewards(data);
|
||
daily.selectWeeklyReward(null, 'weekly_140');
|
||
assert.equal(preview.active, true); assert.equal(requests.length, 0);
|
||
assert.equal(weekly.getChildByName('tiers').getChildByName('weekly_140').getChildByName('claimed').active, true);
|
||
assert.equal(preview.getChildByName('rewards').children.filter(node => node.active).length, 3);
|
||
});
|
||
test('capture listener closes outside touches without a dismiss overlay and preserves inside touches', () => {
|
||
const { daily, weekly, preview } = setup(0); daily.selectWeeklyReward(null, 'weekly_35');
|
||
const listener = daily.node.listeners[0]; assert.equal(listener.capture, true); assert.equal(listener.type, 'touchstart');
|
||
let stopped = 0;
|
||
const inside = { getLocation: () => ({ x: daily.node.x + preview.x, y: daily.node.y + weekly.y + preview.y }), stopPropagation: () => stopped++ };
|
||
listener.handler.call(listener.target, inside); assert.equal(preview.active, true); assert.equal(stopped, 0);
|
||
const outside = { getLocation: () => ({ x: daily.node.x - 500, y: daily.node.y + 600 }), stopPropagation: () => stopped++ };
|
||
listener.handler.call(listener.target, outside); assert.equal(preview.active, false); assert.equal(stopped, 1);
|
||
daily.selectWeeklyReward(null, 'weekly_70'); assert.equal(preview.active, true);
|
||
daily.onDestroy(); assert.equal(daily.node.listeners.length, 0);
|
||
});
|
||
|
||
test('failed requests allow retry and slow requests do not mark a chest claimed', () => {
|
||
const { daily, weekly, requests, toasts, tweens, advance } = setup(35); daily.selectWeeklyReward(null, 'weekly_35');
|
||
requests[0][1]({ code: 0, msg: '领取失败' });
|
||
const check = weekly.getChildByName('tiers').getChildByName('weekly_35').getChildByName('claimed');
|
||
assert.equal(check.active, false); assert.equal(daily.touchStart, true); assert.deepEqual(toasts, ['领取失败']);
|
||
daily.selectWeeklyReward(null, 'weekly_35'); assert.equal(requests.length, 2); daily.timeout();
|
||
assert.equal(daily.touchStart, true); assert.equal(check.active, false);
|
||
assert.equal(tweens.length, 1); assert.equal(tweens[0].cancelled, false);
|
||
advance(0.12); assert.equal(weekly.getChildByName('tiers').getChildByName('weekly_35').getChildByName('chest').angle, 6);
|
||
});
|
||
test('edge previews stay inside the panel and reuse editor-created reward nodes', () => {
|
||
const { daily, weekly, preview, cc } = setup(0); const items = preview.getChildByName('rewards').children, original = items.slice();
|
||
cc.instantiate = () => { throw Error('weekly rewards must not instantiate nodes'); };
|
||
const tier = weekly.getChildByName('tiers').children[0]; tier.x = -120; daily.renderWeeklyRewards(cc.fx.GameConfig.GM_INFO.tasks); assert.equal(tier.x, -120);
|
||
daily.selectWeeklyReward(null, 'weekly_140'); assert.equal(preview.width, 508); assert.equal(preview.x, 136); assert.equal(preview.getChildByName('arrow').x, 204);
|
||
assert.equal(items.filter(node => node.active).length, 3);
|
||
daily.selectWeeklyReward(null, 'weekly_35'); assert.equal(items.filter(node => node.active).length, 2);
|
||
items.forEach((node, index) => assert.equal(node, original[index]));
|
||
});
|
||
test('preview arrow keeps the same gap above each moved chest, including the lower final chest', () => {
|
||
const { daily, data, weekly, preview } = setup(0);
|
||
const tiers = weekly.getChildByName('tiers');
|
||
tiers.x = 12; tiers.y = 7; tiers.scaleX = 0.9; tiers.scaleY = 0.8;
|
||
const heights = [];
|
||
tiers.children.forEach((tier, index) => {
|
||
const chest = tier.getChildByName('chest');
|
||
chest.x = 5; chest.scaleY = 1.1;
|
||
data.weeklyRewards[index].rewards = Array.from({ length: index === 3 ? 5 : 3 }, () => ({ type: 'coin', count: 100 }));
|
||
daily.selectWeeklyReward(null, tier.name);
|
||
const top = weekly.convertToNodeSpaceAR(chest.convertToWorldSpaceAR({ x: 0, y: 0 }));
|
||
const arrow = preview.getChildByName('arrow');
|
||
assert.ok(Math.abs(preview.y + arrow.y - arrow.height * arrow.anchorY - top.y - 24) < 1e-8);
|
||
assert.ok(Math.abs(preview.x + arrow.x - top.x) < 1e-8);
|
||
heights.push(preview.y - preview.height / 2);
|
||
});
|
||
assert.ok(heights[3] < heights[2], 'the final popup follows its lower chest even with two rows of rewards');
|
||
});
|
||
|
||
test('progress remains bounded and empty weekly data hides the section', () => {
|
||
const { daily, data, weekly } = setup(0); assert.equal(weekly.getChildByName('progress').getComponent('cc.ProgressBar').progress, 0);
|
||
data.weeklyStars = 999; daily.renderWeeklyRewards(data); assert.equal(weekly.getChildByName('progress').getComponent('cc.ProgressBar').progress, 1);
|
||
data.weeklyRewards = []; daily.renderWeeklyRewards(data); assert.equal(weekly.active, false);
|
||
});
|
||
|
||
test('five weekly rewards reuse editor slots in centered rows with a fixed arrow tip', () => {
|
||
const { daily, data, preview, cc } = setup(0);
|
||
const rewards = [{ type: 'coin', count: 2000 }, { type: 'infinite_health', count: 3600 }, { type: 'freeze', count: 1 }, { type: 'magic_wand', count: 1 }, { type: 'hammer', count: 1 }];
|
||
data.weeklyRewards[3].rewards = rewards;
|
||
cc.instantiate = () => { throw Error('preview must not create reward nodes'); };
|
||
daily.selectWeeklyReward(null, 'weekly_140');
|
||
const list = preview.getChildByName('rewards'), slots = list.children.filter(n => n.active);
|
||
assert.equal(slots.length, 5); assert.equal(preview.width, 508); assert.equal(preview.height, 360);
|
||
assert.deepEqual(slots.map(n => [n.x, n.y]), [[-164, 90], [0, 90], [164, 90], [-82, -90], [82, -90]]);
|
||
assert.equal(preview.y + preview.getChildByName('arrow').y - 10, 65 + 24);
|
||
assert.deepEqual(slots.map(n => countText(n.getChildByName('count'))), ['2000', '60分钟', '×1', '×1', '×1']);
|
||
daily.selectWeeklyReward(null, 'weekly_35');
|
||
assert.equal(preview.height, 180); assert.equal(preview.y, 219); assert.equal(list.children.filter(n => n.active).length, 2);
|
||
});
|
||
|
||
test('coin and infinite health icons use the configured 0.75 scale while other rewards retain their size', () => {
|
||
const { daily, cc } = setup(0);
|
||
const list = daily.content.getChildByName('taskItem').getChildByName('rewards');
|
||
const stars = list.children[0].getChildByName('stars');
|
||
const originalStars = [stars.width, stars.height, stars.scaleX, stars.scaleY];
|
||
const coinRect = daily.rewardIcons[0].getRect();
|
||
for (const [index, type] of ['coin', 'freeze', 'hammer', 'magic_wand', 'infinite_health'].entries()) {
|
||
// Reuse the same node, including after the much larger heart/wand assets.
|
||
daily.renderTaskRewards(list, { stars: 2, rewards: [{ type, count: 1 }] });
|
||
const icon = list.children[0].getChildByName('icon'), rect = daily.rewardIcons[index].getRect();
|
||
assert.ok(Math.abs(icon.height * icon.scaleY - coinRect.height * ([0, 4].includes(index) ? 0.75 : 1)) < 1e-8, `${type} has the expected display height`);
|
||
assert.equal(icon.width / icon.height, rect.width / rect.height, `${type} retains its trimmed aspect ratio`);
|
||
assert.equal(icon.scaleX, icon.scaleY);
|
||
assert.equal(icon.getComponent('cc.Sprite').sizeMode, cc.Sprite.SizeMode.TRIMMED);
|
||
assert.equal(icon.getComponent('cc.Sprite').trim, true);
|
||
assert.deepEqual([stars.width, stars.height, stars.scaleX, stars.scaleY], originalStars);
|
||
if (type === 'coin') assert.equal(icon.scale, 0.75, 'coin uses the configured reduced size');
|
||
}
|
||
daily.renderTaskRewards(list, { stars: 2, rewards: [{ type: 'rocket', count: 1 }] });
|
||
const icon = list.children[0].getChildByName('icon');
|
||
assert.equal(icon.getComponent('cc.Sprite').spriteFrame, null);
|
||
assert.equal(icon.scale, 1, 'missing assets do not retain a previous reward scale');
|
||
daily.renderTaskRewards(list, { stars: 2, rewards: [{ type: 'coin', count: 100 }] });
|
||
assert.equal(icon.scale, 0.75);
|
||
assert.equal(icon.height, coinRect.height);
|
||
});
|
||
|
||
test('preview icons match the outer coin height and retain image counts without action buttons', () => {
|
||
const { daily, preview } = setup(0); daily.selectWeeklyReward(null, 'weekly_35');
|
||
const rewards = preview.getChildByName('rewards'); assert.equal(rewards.scaleX, 1);
|
||
assert.equal(preview.width, 344); assert.equal(preview.height, 180);
|
||
for (const slot of rewards.children.filter(slot => slot.active)) {
|
||
assert.equal(slot.scaleX, 1);
|
||
const icon = slot.getChildByName('icon');
|
||
assert.equal(icon.scaleX, icon.scaleY);
|
||
const frame = icon.getComponent('cc.Sprite').spriteFrame;
|
||
if (frame) {
|
||
assert.ok(Math.abs(icon.height * icon.scaleY - daily.rewardIcons[0].getRect().height * ([daily.rewardIcons[0], daily.rewardIcons[4]].includes(icon.getComponent('cc.Sprite').spriteFrame) ? 0.75 : 1)) < 1e-8);
|
||
assert.equal(icon.width / icon.height, frame.getRect().width / frame.getRect().height);
|
||
}
|
||
assert.ok(slot.getChildByName('count').getChildByName('RewardCountGlyphs').children.filter(glyph => glyph.active).every(glyph => glyph.height === 40));
|
||
assert.equal(slot.getChildByName('plus').active, false);
|
||
}
|
||
assert.equal(preview.getChildByName('btnClaim'), undefined); assert.equal(preview.getChildByName('close'), undefined);
|
||
});
|
||
test('missing reward images keep empty icon slots and quantities without text substitutes', () => {
|
||
const { daily, preview } = setup(0);
|
||
daily.selectWeeklyReward(null, 'weekly_35');
|
||
daily.selectWeeklyReward(null, 'weekly_140');
|
||
const items = preview.getChildByName('rewards').children;
|
||
for (const slot of items.slice(0, 2)) {
|
||
assert.equal(slot.active, true);
|
||
assert.equal(slot.getChildByName('icon').active, true);
|
||
assert.equal(slot.getChildByName('icon').getComponent('cc.Sprite').spriteFrame, null);
|
||
assert.equal(slot.getChildByName('rewardName').active, false);
|
||
assert.equal(slot.getChildByName('rewardName').getComponent('cc.Label').string, '');
|
||
assert.equal(countText(slot.getChildByName('count')), '15分钟');
|
||
}
|
||
daily.rewardIcons[8] = { getRect: () => ({ width: 100, height: 200 }) };
|
||
daily.selectWeeklyReward(null, 'weekly_140');
|
||
assert.equal(items[0].getChildByName('icon').getComponent('cc.Sprite').spriteFrame, daily.rewardIcons[8]);
|
||
});
|
||
|
||
test('runtime layout repairs the old 0.82 x 0.8 scaling without creating nodes', () => {
|
||
const { daily, preview, cc } = setup(0), rewards = preview.getChildByName('rewards');
|
||
rewards.scale = 0.82;
|
||
rewards.children.forEach(slot => { slot.scale = 0.8; slot.getChildByName('icon').setContentSize(90, 94); });
|
||
cc.instantiate = () => { throw Error('no runtime nodes'); };
|
||
daily.selectWeeklyReward(null, 'weekly_35');
|
||
assert.equal(rewards.scale, 1); assert.equal(preview.width, 344);
|
||
for (const slot of rewards.children) {
|
||
assert.equal(slot.scale, 1);
|
||
const icon = slot.getChildByName('icon');
|
||
if (icon.getComponent('cc.Sprite').spriteFrame) assert.ok(Math.abs(icon.height * icon.scaleY - daily.rewardIcons[0].getRect().height * ([daily.rewardIcons[0], daily.rewardIcons[4]].includes(icon.getComponent('cc.Sprite').spriteFrame) ? 0.75 : 1)) < 1e-8);
|
||
}
|
||
daily.selectWeeklyReward(null, 'weekly_140'); assert.equal(preview.width, 508);
|
||
assert.equal(preview.getComponent('cc.Sprite').type, 1);
|
||
});
|
||
|
||
test('outside hit testing converts camera screen coordinates before checking the preview', () => {
|
||
const { daily, weekly, preview, cc } = setup(0); daily.selectWeeklyReward(null, 'weekly_35');
|
||
const insideWorld = { x: daily.node.x + preview.x, y: daily.node.y + weekly.y + preview.y };
|
||
cc.Camera.findCamera = () => ({ getScreenToWorldPoint: (point, out) => { out.x = point.x + 700; out.y = point.y - 300; return out; } });
|
||
let stopped = 0;
|
||
daily.onWeeklyPreviewOutsideTouch({ getLocation: () => ({ x: insideWorld.x - 700, y: insideWorld.y + 300 }), stopPropagation: () => stopped++ });
|
||
assert.equal(preview.active, true); assert.equal(stopped, 0);
|
||
daily.onWeeklyPreviewOutsideTouch({ getLocation: () => ({ x: -1000, y: -1000 }), stopPropagation: () => stopped++ });
|
||
assert.equal(preview.active, false); assert.equal(stopped, 1);
|
||
});
|