fix: fit activity amounts and show unpaid completion badge
This commit is contained in:
parent
96a5e1acf9
commit
7fb430e512
|
|
@ -121,7 +121,11 @@ export default class GoldMinerService {
|
|||
}
|
||||
get settlementNotice() { return Object.keys(this.ledger.notices).reduce((sum, id) => sum + this.ledger.notices[id], 0); }
|
||||
dismissNotice() { this.ledger.notices = {}; this.persist(); }
|
||||
get hasBadge() { return this.settlementNotice > 0 || !!(this.info && this.info.tasks.some(t => t.claimStatus === 'claimable' || t.claimStatus === 'issuing')); }
|
||||
get hasBadge() {
|
||||
return this.settlementNotice > 0 || !!(this.info && this.info.tasks.some(t =>
|
||||
t.claimStatus !== 'claimed' && (t.claimStatus === 'claimable' || t.claimStatus === 'issuing'
|
||||
|| t.claimStatus === 'pending_unlock' || this.info.progressWins >= t.targetWins)));
|
||||
}
|
||||
stopPolling() { this.polling++; }
|
||||
/** First validated snapshot; recovery continues on the existing serialized queue. */
|
||||
readForDisplay(): Promise<any> {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ info ← 首份数据返回后即可显示入口/主面
|
|||
|
||||
### 入口显示与布局
|
||||
|
||||
- 有补发到账通知或可领取/发放中的奖励时,入口可显示红点。
|
||||
- 有补发到账通知,或存在未领取的已达标任务时,入口显示提示标记(叹号);未购买也提示。任务 pending_unlock、claimable、issuing 或累计进度达到目标都可触发,claimed 不重复提示。活动结束后仍按 Host 的期次/补发规则决定入口是否保留。
|
||||
- 普通展示还要求 canIos 开关允许,且 info.status 为 purchasable、purchase_disabled 或 unlocked。
|
||||
- locked、unavailable 默认不显示入口,奖励通知是例外。
|
||||
- 使用 HomeScene 的 Load/Top/CoinMadnessEntry;找不到时才创建。
|
||||
|
|
@ -274,6 +274,8 @@ confirm_settlement_delivery 请求携带 settlementId、grantIds;当前逐档
|
|||
|
||||
## 11. 排查与验证
|
||||
|
||||
数字适配:总奖励限制在 164 像素、每档奖励限制在 170 像素的局部宽度内,按实际字形宽度等比缩放并保留正字间距。价格与“元”整体居中,数字变长时单位跟随移动,超过按钮可用宽度才缩小。离线预览检查 `check-number-layout.cjs` 覆盖四/五位奖励、一/二/三位价格、重复刷新和未购买达标提示;运行前启动 `preview.cjs`(检查默认端口 8879,可用 PORT 指定)。
|
||||
|
||||
2026-09-30 清理:已删除游戏测试分包、活动测试面板、首页测试入口及两个演示场景。保留活动正式流程、支付诊断日志和离线回归检查。
|
||||
|
||||
- 进度不涨:查看真实通关那条 userLevel 的请求 goldMiner 与响应 data.goldMiner,再对比 info 的 periodId/progressWins/tasks。
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ export default class CoinMadnessDigits extends cc.Component {
|
|||
@property gap = 0;
|
||||
@property colonWidth = .36;
|
||||
private text = '';
|
||||
setText(text: string) {
|
||||
if (this.text === text) return;
|
||||
if (text.length > this.node.children.length) { cc.warn('Coin Madness digit capacity exceeded', text); return; }
|
||||
private widthLimit = -1;
|
||||
private renderedWidth = 0;
|
||||
setText(text: string, maxWidth = 0): number {
|
||||
if (this.text === text && this.widthLimit === maxWidth) return this.renderedWidth;
|
||||
if (text.length > this.node.children.length) { cc.warn('Coin Madness digit capacity exceeded', text); return this.renderedWidth; }
|
||||
this.text = text;
|
||||
this.widthLimit = maxWidth;
|
||||
const glyphs = text.split('').map(char => {
|
||||
// The countdown separator is intentional text, never a missing-image fallback.
|
||||
if (char === ':') return { char, frame: null, width: this.height * this.colonWidth };
|
||||
|
|
@ -20,14 +23,20 @@ export default class CoinMadnessDigits extends cc.Component {
|
|||
const rect = frame.getRect();
|
||||
return { char, frame, width: this.height * rect.width / rect.height };
|
||||
});
|
||||
let x = -(glyphs.reduce((sum, glyph) => sum + glyph.width, 0) + this.gap * (glyphs.length - 1)) / 2;
|
||||
// Bounded amounts need clear spacing, including outlines on the digit artwork.
|
||||
const gap = maxWidth > 0 ? Math.max(2, this.gap) : this.gap;
|
||||
const width = glyphs.reduce((sum, glyph) => sum + glyph.width, 0) + gap * Math.max(0, glyphs.length - 1);
|
||||
const scale = maxWidth > 0 && width > maxWidth ? maxWidth / width : 1;
|
||||
this.renderedWidth = width * scale;
|
||||
let x = -this.renderedWidth / 2;
|
||||
this.node.children.forEach((slot, i) => {
|
||||
slot.active = i < glyphs.length; if (!slot.active) return;
|
||||
const glyph = glyphs[i]; slot.x = x + glyph.width / 2;
|
||||
const glyph = glyphs[i]; slot.x = x + glyph.width * scale / 2;
|
||||
const image = slot.getChildByName('Image');
|
||||
image.active = glyph.char !== ':';
|
||||
if (glyph.char !== ':') { image.getComponent(cc.Sprite).spriteFrame = glyph.frame; image.setContentSize(glyph.width, this.height); }
|
||||
x += glyph.width + this.gap;
|
||||
if (glyph.char !== ':') { image.getComponent(cc.Sprite).spriteFrame = glyph.frame; image.setContentSize(glyph.width * scale, this.height * scale); }
|
||||
x += (glyph.width + gap) * scale;
|
||||
});
|
||||
return this.renderedWidth;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,13 +132,20 @@ export default class CoinMadnessPanel extends cc.Component {
|
|||
this.unlockedOffer.active = unlocked;
|
||||
this.purchaseButton.active = !unlocked && (!this.api.request || data.status === 'purchasable');
|
||||
this.totalDigits.node.position = unlocked ? this.unlockedTotalPosition.position : this.lockedTotalPosition;
|
||||
this.totalDigits.setText(String(config.stages.reduce((sum, s) => sum + s.coins, 0)));
|
||||
this.totalDigits.setText(String(config.stages.reduce((sum, s) => sum + s.coins, 0)), 164);
|
||||
this.purchaseLabel.string = expired ? '已结束' : state.stage >= config.stages.length ? '已全部领取' : state.paidAt ? '已解锁' : '';
|
||||
const canBuy = !state.paidAt && !expired && (!this.api.request || data.status === 'purchasable');
|
||||
this.purchaseButton.color = canBuy ? cc.Color.WHITE : new cc.Color(200, 195, 180);
|
||||
this.priceDigits.node.active = this.currency.active = canBuy;
|
||||
if (this.discountDigits) this.discountDigits.setText(String(this.discount));
|
||||
if (Number.isFinite(config.priceCents)) this.priceDigits.setText(String(Math.floor(config.priceCents / 100)));
|
||||
if (Number.isFinite(config.priceCents)) {
|
||||
const currencyWidth = this.currency.width * Math.abs(this.currency.scaleX), gap = 12;
|
||||
const width = this.priceDigits.setText(String(Math.floor(config.priceCents / 100)),
|
||||
Math.max(1, this.purchaseButton.width - 64 - currencyWidth - gap));
|
||||
// Center the price and currency together instead of keeping “元” at a fixed x.
|
||||
this.priceDigits.node.x = -(currencyWidth + gap) / 2;
|
||||
this.currency.x = (width + gap) / 2;
|
||||
}
|
||||
// The first complete prefab row is the template for additional configured stages.
|
||||
while (this.content.children.length < config.stages.length) {
|
||||
const row = cc.instantiate(this.content.children[0]); this.content.addChild(row);
|
||||
|
|
@ -188,7 +195,7 @@ export default class CoinMadnessPanel extends cc.Component {
|
|||
lock.getComponent(cc.Sprite).spriteFrame = this.lockedFrame;
|
||||
} else if (!lock.getNumberOfRunningActions()) lock.active = false;
|
||||
this.paintProgress(row, i, expired);
|
||||
child('Reward').getComponent(CoinMadnessDigits).setText(String(stage.coins));
|
||||
child('Reward').getComponent(CoinMadnessDigits).setText(String(stage.coins), 170);
|
||||
['Task', 'CompletedTask'].forEach(name => child(name).getChildByName('Target').getComponent(CoinMadnessDigits).setText(String(stage.wins)));
|
||||
});
|
||||
this.focusGrowingTask();
|
||||
|
|
|
|||
76
tools/coin-madness/check-number-layout.cjs
Normal file
76
tools/coin-madness/check-number-layout.cjs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const ts = require('typescript');
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const output = ts.transpileModule(fs.readFileSync('assets/Script/coin_madness/GoldMinerService.ts', 'utf8'), {
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS }
|
||||
}).outputText;
|
||||
const exported = {};
|
||||
new Function('require', 'exports', output)(() => ({}), exported);
|
||||
const service = Object.create(exported.default.prototype);
|
||||
service.ledger = { notices: {} };
|
||||
for (const status of ['purchasable', 'unlocked']) {
|
||||
service.info = { status, progressWins: 4, tasks: [{ targetWins: 5, claimStatus: 'locked' }] };
|
||||
assert.equal(service.hasBadge, false);
|
||||
service.info.progressWins = 5;
|
||||
assert.equal(service.hasBadge, true);
|
||||
service.info.tasks[0].claimStatus = 'claimed';
|
||||
assert.equal(service.hasBadge, false);
|
||||
}
|
||||
service.info.tasks[0].claimStatus = 'pending_unlock';
|
||||
assert.equal(service.hasBadge, true);
|
||||
service.info = null; service.ledger.notices = { old: 100 };
|
||||
assert.equal(service.hasBadge, true);
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true, channel: 'msedge' });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 450, height: 800 } });
|
||||
page.on('pageerror', error => console.error('Preview:', error.message));
|
||||
await page.goto('http://127.0.0.1:' + (process.env.PORT || 8879));
|
||||
await page.waitForFunction(() => window.demo && demo.ready);
|
||||
await page.evaluate(() => {
|
||||
const data = { serverNow: Date.now(), status: 'purchasable', state: {
|
||||
config: { periodId: 'layout-test', startsAt: Date.now() - 1000, endsAt: Date.now() + 3600000,
|
||||
priceCents: 300, stages: Array.from({ length: 5 }, (_, i) => ({ wins: i + 1, coins: 1000 })) },
|
||||
totalWins: 0, paidAt: 0, claimed: Array(5).fill(false), stage: 0 } };
|
||||
window.testService = { info: { ...data.state.config, status: data.status },
|
||||
settlementNotice: 0, serverNow: Date.now(), hasBadge: false,
|
||||
readForDisplay: async () => data, refresh: async () => data, view: () => data, stopPolling() {} };
|
||||
cc.director.getScene().children[0].getComponent('CoinMadnessHost').resumeSync();
|
||||
});
|
||||
await page.evaluate(() => { demo.open(); });
|
||||
await page.waitForFunction(() => cc.director.getScene().children[0].getChildByName('CoinMadness')?.getComponent('CoinMadnessPanel')?.data);
|
||||
for (const price of [3, 68, 128, 3]) {
|
||||
const result = await page.evaluate(price => {
|
||||
const panel = demo.panel(), data = JSON.parse(JSON.stringify(panel.data));
|
||||
data.state.config.priceCents = price * 100;
|
||||
data.state.config.stages.forEach((s, i) => { s.coins = i ? 9999 : 45000; });
|
||||
data.state.paidAt = 0; data.status = 'purchasable';
|
||||
panel.render(data);
|
||||
function bounds(digits) {
|
||||
const boxes = digits.node.children.filter(n => n.active).map(n => {
|
||||
const image = n.getChildByName('Image');
|
||||
return { left: n.x - image.width / 2, right: n.x + image.width / 2 };
|
||||
});
|
||||
return { left: boxes[0].left, right: boxes[boxes.length - 1].right,
|
||||
separated: boxes.every((b, i) => !i || b.left >= boxes[i - 1].right) };
|
||||
}
|
||||
const p = bounds(panel.priceDigits), total = bounds(panel.totalDigits);
|
||||
return { total, price: p, priceRight: panel.priceDigits.node.x + p.right,
|
||||
currencyLeft: panel.currency.x - panel.currency.width / 2,
|
||||
currencyRight: panel.currency.x + panel.currency.width / 2,
|
||||
buttonRight: panel.purchaseButton.width / 2,
|
||||
rewards: panel.content.children.filter(n => n.active).map(n => bounds(n.getChildByName('Reward').getComponent('CoinMadnessDigits'))) };
|
||||
}, price);
|
||||
assert(result.total.separated && result.total.right - result.total.left <= 164.01);
|
||||
assert(result.price.separated && result.priceRight < result.currencyLeft);
|
||||
assert(result.currencyRight < result.buttonRight);
|
||||
assert(result.rewards.every(r => r.separated && r.right - r.left <= 170.01));
|
||||
fs.mkdirSync('.codex_tmp/number-layout', { recursive: true });
|
||||
await page.screenshot({ path: '.codex_tmp/number-layout/price-' + price + '.png' });
|
||||
}
|
||||
console.log('PASS: 4/5-digit rewards, 1/2/3-digit prices, repeated layout and unpaid completion badge');
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
|
|
@ -55,7 +55,7 @@ const server = http.createServer((req,res)=>{
|
|||
if(url==='/host.js'){
|
||||
const source=fs.readFileSync(path.join(root,'assets/Script/coin_madness/CoinMadnessHost.ts'),'utf8');
|
||||
const output=ts.transpileModule(source,{compilerOptions:{target:ts.ScriptTarget.ES5,module:ts.ModuleKind.CommonJS,experimentalDecorators:true}}).outputText;
|
||||
res.setHeader('Content-Type','text/javascript');return res.end('(function(){var exports={};function require(name){if(name.includes("GoldMinerService"))return {default:{testEnabled:()=>!!window.testService?.testAvailable(),current:()=>window.testService||null}};if(name.includes("MiniGameSdk"))return {MiniGameSdk:{API:{showToast:console.log}}};return {default:{get:()=>({enqueue:(k,p,run)=>run(()=>{})})}};}cc._RF.push({},"CoinMadnessHost","CoinMadnessHost");'+output+';cc._RF.pop();})();');
|
||||
res.setHeader('Content-Type','text/javascript');return res.end('(function(){var exports={};function require(name){if(name.includes("GoldMinerService"))return {default:{testEnabled:()=>!!window.testService?.testAvailable(),current:()=>window.testService||null}};if(name.includes("Utils"))return {default:{serverUrl:()=>"fixture://"}};if(name.includes("MiniGameSdk"))return {MiniGameSdk:{API:{showToast:console.log}}};return {default:{get:()=>({enqueue:(k,p,run)=>run(()=>{})})}};}cc._RF.push({},"CoinMadnessHost","CoinMadnessHost");'+output+';cc._RF.pop();})();');
|
||||
}
|
||||
if(/^\/native\/[a-f0-9]{2}\/[a-f0-9-]+\.png$/.test(url)){
|
||||
const target=path.join(root,'library/imports',url.slice('/native/'.length));
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user