fix(studio): B2′ 真人试玩闭环 3 处修复 — 布局高度链/demo兜底非致命/game_end补completed

浏览器实玩(staging+真无头Chrome)首次暴露、curl/单测均未现形的 3 个真 bug:

1) 布局:#app/.app-root 改 height:100% 建确定高度链——原 min-height 链断,子页 .feed/.play 的 height:100% 塌成 0,封面/画布 0 高不可点(真机也塌)。

2) GamePlayer:取包/manifest 校验失败回退 demo 是「可恢复」场景,不再 emit('error')(父级 Play 把 error 当致命态 v-else-if 隐藏宿主显示「加载失败」,与 demo 兜底冲突),仅 console.warn。

3) runtime:clicker finish() 补发 completed:true+duration_ms——后端 quality_score 完玩率口径依赖 props.completed,缺它真玩也推不动 quality(数据回路失效)。

实证:浏览器玩 A-小卖部通关→quality 0→60→feed 从 B 领头(30)翻转为 A 登顶(60)。vue-tsc -b 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-09 17:01:04 +00:00
parent ab0037cd4b
commit d279898192
4 changed files with 18 additions and 7 deletions

View File

@ -32,7 +32,8 @@ const showTabBar = computed(() => !route.meta?.hideTabBar)
.app-root {
display: flex;
flex-direction: column;
/* 用动态视口高度,移动端地址栏收缩时不抖动 */
/* height:100% 填满 #app(确定高度链),使子页 height:100% 可解析;min-height:100dvh 作下限兼顾移动端动态视口收缩不抖动 */
height: 100%;
min-height: 100vh;
min-height: 100dvh;
background: var(--bg);

View File

@ -118,9 +118,11 @@ async function resolvePackage(): Promise<GamePackage> {
const fetcher = props.fetchPackage ?? getRuntimePackage;
respVO = await fetcher(vid, props.mode);
} catch (e) {
// 取清单失败:抛给上层 error,并以 demo 兜底(不白屏,便于自测链路)
// 取清单失败:可恢复场景——demo 兜底即可正常试玩,故不向上 emit('error')
// (父级 Play 把 error 当致命态、会 v-else-if 隐藏宿主显示「加载失败」,与 demo 兜底冲突)。仅 console.warn 记录。
const message = e instanceof Error ? e.message : '取包清单失败';
emit('error', { message });
// eslint-disable-next-line no-console
console.warn('[GamePlayer] 取包清单失败,回退 demo 兜底(不致命):', message);
return buildDemoPackage(props.gameId, props.versionId, traceId.value);
}
@ -129,9 +131,11 @@ async function resolvePackage(): Promise<GamePackage> {
try {
return await fetchAndVerifyManifest(respVO.manifestUrl, respVO.checksum);
} catch (e) {
// 拉取/校验失败:上报 error,退回 demo 兜底(去无条件 demo,仅失败时兜底)
// 拉取/校验失败:可恢复场景——退回 demo 兜底即可正常试玩,故不向上 emit('error')
// (父级 Play 把 error 当致命态、会 v-else-if 隐藏宿主显示「加载失败」,与 demo 兜底冲突)。仅 console.warn 记录。
const message = e instanceof Error ? e.message : 'manifest 取包/校验失败';
emit('error', { message });
// eslint-disable-next-line no-console
console.warn('[GamePlayer] manifest 校验失败,回退 demo 兜底(不致命):', message);
const demo = buildDemoPackage(props.gameId, props.versionId, respVO.traceId || traceId.value);
demo.templateId = respVO.templateId || demo.templateId;
return demo;

View File

@ -73,6 +73,7 @@ export function startRuntime(
let running = true; // 主循环开关
let rafId = 0; // requestAnimationFrame 句柄
let started = false; // 是否已发 game_start
let startMs = 0; // 本局开始时间戳(game_start 时记;用于 game_end 的 duration_ms)
// 目标分数:优先取 gameConfig.target,缺省 5(demo 玩法 = 点满即结束)
const target =
typeof pkg.gameConfig?.target === 'number' ? (pkg.gameConfig.target as number) : 5;
@ -143,6 +144,7 @@ export function startRuntime(
if (!running) return;
if (!started) {
started = true;
startMs = Date.now();
life('game_start');
}
score += 1;
@ -160,7 +162,9 @@ export function startRuntime(
running = false;
cancelAnimationFrame(rafId);
canvas.removeEventListener('pointerdown', onPointer);
life('game_end', { score });
// game_end 必须带 completed + duration_ms:后端 quality_score 完玩率口径依赖 props.completed
// (达 target 即通关 = completed:true);缺它则 completeRate=0、quality 永不上升,数据回路失效(B2′ 实测)。
life('game_end', { score, completed: true, duration_ms: startMs ? Date.now() - startMs : 0 });
}
// 绑定交互

View File

@ -80,7 +80,9 @@ a {
/* 应用根容器:撑满视口、单列竖屏 */
#app {
min-height: 100%;
/* 用 height(而非 min-height)建立确定高度链:使子页 .feed/.play 的 height:100% 能解析。
否则祖先全是 min-height → 子页百分比高度无确定基准 → 塌成内容高,封面/画布 0 高不可点(B2′ 实测)。 */
height: 100%;
display: flex;
flex-direction: column;
}