Files
MatchMaster/tools/preview-level-reward.cjs

147 lines
9.9 KiB
JavaScript

// Isolated Cocos preview: production reward renderer, Win prefab geometry and original artwork.
// node tools/preview-level-reward.cjs [--check] (PLAYWRIGHT_MODULE can point to an installed package).
const http = require('node:http'), fs = require('node:fs'), path = require('node:path');
const { loadMembers } = require('./new-player-ab-test-support.cjs');
const root = path.resolve(__dirname, '..');
const creator = process.env.COCOS_CREATOR_PATH || 'C:/ProgramData/cocos/editors/Creator/2.4.15';
const port = Number(process.env.REWARD_PREVIEW_PORT || 8795);
const images = ['assets/publicUI/font.png', 'assets/res/move/jibijj.png',
'assets/win/texture/juli.png', 'assets/win/texture/newMode1.png'];
const data = {
prefab: JSON.parse(fs.readFileSync(path.join(root, 'assets/win/prefab/Win.prefab'))),
atlases: images.map(file => ({ file, meta: JSON.parse(fs.readFileSync(path.join(root,
file === images[0] ? 'assets/publicUI/font.plist.meta' : file + '.meta'))) })),
};
const code = `window.NumberToImage=${loadMembers('assets/Script/NumberToImage.ts', ['numberToImageNodes']).toString()};
window.RewardDisplay=${loadMembers('assets/Script/SceneManager.ts', ['updateWinCoin', 'updateWinCoinIconState']).toString()};`;
async function boot() {
window.errors = [];
window.addEventListener('error', event => errors.push(String(event.error || event.message)));
window.addEventListener('unhandledrejection', event => errors.push(String(event.reason)));
const data = await fetch('/data').then(r => r.json());
cc.assetManager.init({ importBase: '/imports', nativeBase: '/native' });
cc.assetManager.loadBundle('/internal', error => {
if (error) throw error;
cc.game.run({ id: 'GameCanvas', debugMode: 1, frameRate: 30, renderMode: 0,
groupList: ['default'], collisionMatrix: [[true]] }, async () => {
const scene = new cc.Scene(), canvas = new cc.Node('Canvas');
scene.addChild(canvas); canvas.setContentSize(720, 650);
const component = canvas.addComponent(cc.Canvas);
component.designResolution = cc.size(720, 650); component.fitWidth = component.fitHeight = true;
cc.view.setDesignResolutionSize(720, 650, cc.ResolutionPolicy.SHOW_ALL);
cc.view.resizeWithBrowserSize(true); cc.director.runScene(scene);
const cameraNode = new cc.Node('Camera'); canvas.addChild(cameraNode);
const camera = cameraNode.addComponent(cc.Camera);
camera.backgroundColor = cc.color(236, 221, 189);
cc.fx = { GameConfig: { LEGACY_LAYER_TWO_VARIANT_B_ENABLED: false } };
const frames = {}; NumberToImage.font = { _spriteFrames: {} };
for (const { file, meta } of data.atlases) {
const texture = await new Promise((resolve, reject) => cc.assetManager.loadRemote('/' + file,
(e, asset) => e ? reject(e) : resolve(asset)));
for (const [name, m] of Object.entries(meta.subMetas)) {
const frame = new cc.SpriteFrame(texture, cc.rect(m.trimX, m.trimY, m.width, m.height),
!!m.rotated, cc.v2(m.offsetX, m.offsetY), cc.size(m.rawWidth, m.rawHeight));
frames[m.uuid] = frame;
NumberToImage.font._spriteFrames[name.replace(/\.png$/, '')] = frame;
}
}
function build(index) {
const row = data.prefab[index], node = new cc.Node(row._name), trs = row._trs.array;
node.setPosition(trs[0], trs[1]); node.scaleX = trs[7]; node.scaleY = trs[8];
node.setAnchorPoint(row._anchorPoint.x, row._anchorPoint.y);
node.setContentSize(row._contentSize.width, row._contentSize.height); node.active = row._active;
for (const ref of row._components) {
const component = data.prefab[ref.__id__];
if (component.__type__ === 'cc.Sprite' && component._spriteFrame) {
const sprite = node.addComponent(cc.Sprite); sprite.spriteFrame = frames[component._spriteFrame.__uuid__];
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; sprite.enabled = component._enabled;
node.setContentSize(row._contentSize.width, row._contentSize.height);
}
}
for (const child of row._children) node.addChild(build(child.__id__));
return node;
}
const reward = build(data.prefab.findIndex(row => row._name === 'tiaodik'));
canvas.addChild(reward); reward.y = -40;
const renderer = new RewardDisplay(); renderer.winCoin = reward.getChildByName('coin');
window.showReward = amount => renderer.updateWinCoin(amount);
window.measureReward = () => {
const digits = renderer.winCoin.children.map(node => node.getBoundingBoxToWorld());
const symbol = reward.getChildByName('New Sprite').getBoundingBoxToWorld();
const artwork = reward.getChildByName('beijing').getBoundingBoxToWorld();
const left = symbol.xMin, right = Math.max(...digits.map(rect => rect.xMax));
return { count: digits.length, width: right - left, left, right,
center: (left + right) / 2, artworkCenter: artwork.center.x,
gap: Math.min(...digits.map(rect => rect.xMin)) - symbol.xMax,
insideArtworkWidth: left >= artwork.xMin && right <= artwork.xMax,
digitGaps: digits.slice(1).map((rect, i) => rect.xMin - digits[i].xMax) };
};
showReward(Number(new URLSearchParams(location.search).get('amount')) || 120);
window.ready = true;
});
});
}
const html = `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<style>html,body{margin:0;height:100%;overflow:hidden;background:#ecddbd}canvas{display:block}</style>
<canvas id="GameCanvas"></canvas><script src="/engine.js"></script><script src="/code.js"></script><script src="/boot.js"></script>`;
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://localhost').pathname;
const send = (type, body) => { res.setHeader('Content-Type', type); res.end(body); };
if (url === '/favicon.ico') { res.statusCode = 204; return res.end(); }
if (url === '/') return send('text/html; charset=utf-8', html);
if (url === '/data') return send('application/json', JSON.stringify(data));
if (url === '/code.js') return send('text/javascript', code);
if (url === '/boot.js') return send('text/javascript', '(' + boot.toString() + ')();');
let target;
if (images.includes(url.slice(1))) target = path.join(root, url.slice(1));
if (url === '/engine.js') target = path.join(creator, 'resources/engine/bin/cocos2d-js-for-preview.js');
if (/^\/imports\/[a-f0-9]{2}\/[a-f0-9-]+\.json$/.test(url)) target = path.join(root, 'library', url.slice(1));
if (/^\/native\/[a-f0-9]{2}\/[a-f0-9-]+\.png$/.test(url)) target = path.join(root, 'library/imports', url.slice('/native/'.length));
if (url.startsWith('/internal/') || url.startsWith('/assets/internal/')) {
const base = path.join(creator, 'resources/cocos2d-x/simulator/win32/assets');
const candidate = path.resolve(base, url.replace(/^\/(assets\/)?/, ''));
if (candidate.startsWith(base + path.sep)) target = candidate;
}
if (target && fs.existsSync(target)) {
res.setHeader('Content-Type', target.endsWith('.js') ? 'text/javascript' : target.endsWith('.png') ? 'image/png' : 'application/json');
return fs.createReadStream(target).pipe(res);
}
res.statusCode = 404; res.end('not found');
});
server.listen(port, '127.0.0.1', async () => {
console.log(`http://127.0.0.1:${port}`);
if (!process.argv.includes('--check')) return;
const assert = require('node:assert/strict');
let browser;
try {
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
browser = await chromium.launch({ headless: true, channel: 'msedge' });
const output = path.join(root, 'temp/level-reward'); fs.mkdirSync(output, { recursive: true });
for (const viewport of [{ width: 390, height: 844 }, { width: 720, height: 1280 }]) {
const page = await browser.newPage({ viewport });
const renderErrors = [];
page.on('console', message => { if (message.type() === 'error') renderErrors.push(message.text()); });
await page.goto(`http://127.0.0.1:${port}`);
await page.waitForFunction(() => window.ready);
await page.evaluate(() => new Promise(resolve => cc.director.once(cc.Director.EVENT_AFTER_DRAW, resolve)));
for (const amount of [40, 80, 120, 160, 240, 480, 120, 40]) {
await page.evaluate(value => showReward(value), amount);
await page.evaluate(() => new Promise(resolve => cc.director.once(cc.Director.EVENT_AFTER_DRAW, resolve)));
const result = await page.evaluate(() => measureReward());
assert.equal(result.count, String(amount).length);
assert.ok(Math.abs(result.center - result.artworkCenter) < 0.01, JSON.stringify(result));
assert.equal(result.gap, 20);
assert.ok(result.digitGaps.every(gap => gap >= 0));
assert.equal(result.insideArtworkWidth, true);
if ([40, 120, 480].includes(amount)) await page.screenshot({ path: path.join(output, `${viewport.width}-${amount}.png`) });
console.log(viewport.width, amount, JSON.stringify(result));
}
assert.deepEqual(await page.evaluate(() => errors), []);
assert.deepEqual(renderErrors, []);
await page.close();
}
} catch (error) { console.error(error); process.exitCode = 1; }
finally { if (browser) await browser.close(); server.close(); }
});