feat(polaris): 《山海巡异录》表现层二轮——奖励屏爆字闸+首页国风化+手牌卡面补描述+职业名统一
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled

创始人真机眼验批①③④⑧。① 奖励横幅被战斗结算爆字(delay~1600ms、depth60)压住;新增 BATTLE_ONLY_FX 闸+onBattleScreen(),离战屏抑制战斗结算类演出,过场/奖励/商店类照播。③ drawHome 套 skin 皮肤基元(山水底/卷轴题头/五行相生珠带/职业卡遗物徽记/册页托),去黑底金边方钮,靶位 computeHomeTargets 未动。④ 手牌卡面下半空白根因=battle-core handCards 投影漏 text 字段;补齐+cardFront 下半重排(元素分隔线/描述铺满/脚注)。⑧ 战斗 HUD+地图英雄名去硬编码「巡异使」、统一取 CLASSES[heroClass].name 与首页同源。scene-smoke(home 显示~20→50、battle=42)+battle-core.test 双门 EXIT=0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-08 04:35:44 -07:00
parent 4a2f782a9d
commit ed3d8a9a5d
3 changed files with 81 additions and 19 deletions

View File

@ -558,7 +558,9 @@ export function createBattleCore(deps, opts = {}) {
handCards: state.hand.map((id) => {
const c = CARD_BY_ID[id];
return {
id, name: c.name, cost: c.cost, element: c.element, rarity: c.rarity, type: c.type,
// text:卡牌描述(缺点④「战斗手牌卡面下半空白」根因——旧投影漏了 text,渲染层 cardFront 的
// `card.text` 恒空,故战斗手牌下半无说明;奖励/商店卡因整卡定义展开带 text 才有。此处补齐,与之对齐)。
id, name: c.name, cost: c.cost, element: c.element, rarity: c.rarity, type: c.type, text: c.text,
dmg: c.effects.dmg ? c.effects.dmg.n * (c.effects.dmg.times || 1) : 0,
block: c.effects.block ? c.effects.block.n : 0,
strength: c.effects.strength ? c.effects.strength.n : 0,

View File

@ -19,7 +19,7 @@
'use strict';
import { ELEMENT_CYCLE, ELEMENT_COLORS, ACHIEVEMENTS } from '../data/tables.js';
import { ELEMENT_CYCLE, ELEMENT_COLORS, ACHIEVEMENTS, CLASSES } from '../data/tables.js';
import {
VIEW_W, VIEW_H, ZONES, CARD_W, CARD_H, midY, handTop,
handCardCenter, endTurnRect, mapNodeCenter, rewardCardCenter, rewardSkipRect, hitTarget,
@ -42,6 +42,16 @@ const NODE_COLOR = { battle: 0x3f7fbf, elite: 0xc98f3b, rest: 0x3fa34d, treasure
const BG_SCENE_A = 'bg-scene-01';
const BG_SCENE_B = 'bg-scene-02';
// 缺点①(奖励横幅遮挡)修复用:仅在战斗屏绘制的「结算类」演出事件。这些飘字/爆字锚定敌区、我方与
// 顶栏,是一局战斗的战报;一旦击杀翻入战利屏(screen='reward'),延迟触发的伤害飘字与「总量爆字」
// (48px,depth 60,落点恰在题头匾额「战利·三选一」上)仍会盖住新屏横幅。故战斗类演出只在 battle 屏出;
// 跨屏的过场/奖励/商店类演出(得遗物/收入卡组/幕启/入市…)不在此列,照常播,不受影响。
const BATTLE_ONLY_FX = new Set([
'cardPlayed', 'damage', 'blockGain', 'strengthGain', 'chainLink', 'chainBreak', 'chainGrace',
'zhoutian', 'enemyAction', 'enemyPhase', 'shuffle', 'turnStart', 'statusApplied', 'burnTick',
'thorns', 'toughnessGain', 'hpCost', 'exhaust', 'goldGain',
]);
/**
* 建一局场景(Phaser scene 配置对象;`this`=Phaser.Scene)
* @param {Object} api
@ -202,9 +212,18 @@ export function createRunScene(api) {
try { scene.cameras.main.shake(PRES.shakeMs, PRES.shakeMaxPx / VIEW_H); } catch (e) { /* 无相机不阻 */ }
if (api.debug) api.debug.shakePeakPx = PRES.shakeMaxPx;
}
/** ① 当前是否停在战斗屏(离战屏则抑制战斗类演出,避免延迟触发的残留飘字压住新屏题头匾额)。 */
function onBattleScreen() {
try {
if (api.mode && api.mode() === 'home') return false; // 局外屏无战斗
const r = api.run && api.run();
return !!(r && r.readState().screen === 'battle');
} catch (e) { return false; } // 取态异常按「不在战斗屏」处理(fail-closed:宁可少播一次演出)
}
/** 总量爆字(§7.2:48px 抖 120ms + 屏震):一次结算的伤害之和;演出层独立于重建层,自毁。 */
function burstText(scene, total) {
if (!total || total <= 0) return;
if (!total || total <= 0 || !onBattleScreen()) return; // ① 离战屏不再爆字,防压住战利屏题头
screenShake(scene);
const eCy = VIEW_H * ZONES.enemyZone / 2 + 14;
const t = scene.add.text(VIEW_W / 2, eCy, `${total}`, { fontSize: `${PRES.burstFontPx}px`, color: '#ffd76b', fontFamily: 'serif', fontStyle: 'bold' }).setOrigin(0.5).setDepth(60);
@ -288,7 +307,9 @@ export function createRunScene(api) {
skin.bgCover(L, BG_SCENE_B, VIEW_W, VIEW_H, 0.42);
drawTopStrip(scene, s);
skin.plaque(L, VIEW_W / 2, 48, `山海图 · 第${s.act}`, { w: 220, h: 46, size: 20 });
stxt(scene, VIEW_W / 2, 78, `巡异使 ${s.hero.hp}/${s.hero.hpMax}`, 14, '#bfe3bf');
// 缺点⑧:地图屏英雄名与首页/战斗 HUD 统一取当前职业 tables 名(去硬编码「巡异使」)。
const heroName = (CLASSES[s.heroClass] && CLASSES[s.heroClass].name) || CLASSES.jia.name;
stxt(scene, VIEW_W / 2, 78, `${heroName} ${s.hero.hp}/${s.hero.hpMax}`, 14, '#bfe3bf');
const g = keep(scene, scene.add.graphics().setDepth(1));
// 边(先画,压在节点下):描金细线连脉络。
@ -347,7 +368,10 @@ export function createRunScene(api) {
if (stParts.length) stxt(scene, VIEW_W / 2, eZoneH - 34, stParts.join(' · '), 13, hx(CIHUANG));
// 中段:我方状态条 + 能量宝珠 + 链轨(五行珠+倍率)+ 结束按钮。
const heroLine = `巡异使 ${b.hero.hp}/${b.hero.hpMax}${b.hero.block > 0 ? `${b.hero.block}` : ''}${b.hero.strength > 0 ? ` 力+${b.hero.strength}` : ''}${b.hero.toughness > 0 ? ` 韧+${b.hero.toughness}` : ''}`;
// 缺点⑧(职业名不一致):HUD 显示当前所选职业的 tables 名(巡罡使/璇玑使),与首页职业卡同源,
// 不再硬编码「巡异使」(该名不在职业表内,连甲职业都对不上)。
const heroName = (CLASSES[s.heroClass] && CLASSES[s.heroClass].name) || CLASSES.jia.name;
const heroLine = `${heroName} ${b.hero.hp}/${b.hero.hpMax}${b.hero.block > 0 ? `${b.hero.block}` : ''}${b.hero.strength > 0 ? ` 力+${b.hero.strength}` : ''}${b.hero.toughness > 0 ? ` 韧+${b.hero.toughness}` : ''}`;
stxt(scene, 60, my - 24, heroLine, 13, '#cdeccd', 0.5);
// 能量宝珠。
skin.orb(L, 34, my + 16, 19);
@ -552,32 +576,55 @@ export function createRunScene(api) {
}
// ── 局外三屏(M3 S15;坐标与 layout computeHomeTargets 同源)──────────────────────
// 缺点③(首页太「方方正正」)修复:home 原为满黑底 + 两块黑册页 + 一列金边方按钮,是全款唯一无一件
// 美术、密度最低的一屏。此处套皮肤基元把它拉到与地图/战斗同一密度——满幅山水底、卷轴题头、五行相生
// 珠带(点题核心机制)、职业卡起始遗物徽记、册页承文案——只换表现层,靶位(computeHomeTargets)一格不动。
function drawHome(scene, hs) {
const L = layerFor(scene);
const m = hs.home.meta;
skin.bgCover(L, BG_SCENE_B, VIEW_W, VIEW_H, 0.4);
// 满幅山水底(与地图/战斗同底,压暗略重,去纯黑感)。
skin.bgCover(L, BG_SCENE_B, VIEW_W, VIEW_H, 0.5);
// 题头:卷轴横条背衬 + 匾额(原来匾额光秃浮在黑底上,和别屏的 HUD 语汇脱节)。
skin.hudBanner(L, VIEW_W / 2, VIEW_H * 0.11, VIEW_W - 24, 76);
skin.plaque(L, VIEW_W / 2, VIEW_H * 0.11, '山海巡异录', { w: 280, h: 60, size: 32 });
stxt(scene, VIEW_W / 2, VIEW_H * 0.175, `出巡 ${m.runsPlayed} · 功成 ${m.wins}`, 13, '#e8dcc0');
// 职业二选(选中金框)。
// 五行相生珠带(木火土金水;点题链系机制,程序绘恒渲染,补足「无一件美术」的空)。
ELEMENT_CYCLE.forEach((el, i) => {
const gx = VIEW_W / 2 - 70 + i * 35;
skin.gem(L, gx, VIEW_H * 0.225, 9, ELEMENT_COLORS[el], { lit: true });
keep(scene, scene.add.text(gx, VIEW_H * 0.225, el, { fontSize: '9px', color: '#141210', fontFamily: 'serif', fontStyle: 'bold' }).setOrigin(0.5).setDepth(12));
});
// 职业二选(靶 homeClass 定死 x=±88/y0.34/150×110):册页卡 + 起始遗物徽记 + 描金选中框。
for (const [i, c] of m.classes.entries()) {
const x = VIEW_W / 2 + (i === 0 ? -88 : 88);
const cy = VIEW_H * 0.34;
const sel = hs.home.sel.class === c.id;
skin.panel(L, x, VIEW_H * 0.34, 150, 110, { radius: 12, variant: sel ? 'panel' : 'dark', raster: sel });
if (sel) { const fr = keep(scene, scene.add.graphics().setDepth(11)); fr.lineStyle(3, GOLD, 1); fr.strokeRoundedRect(x - 75, VIEW_H * 0.34 - 55, 150, 110, 12); }
stxt(scene, x, VIEW_H * 0.31, c.name, 18, sel ? INK_TEXT : hx(GOLD));
stxt(scene, x, VIEW_H * 0.36, `气血 ${c.hpMax}`, 12, sel ? INK_TEXT : '#9fc4e8');
stxt(scene, x, VIEW_H * 0.395, c.id === 'jia' ? '攻伐见长' : '灵脉见长', 11, sel ? '#5a4a30' : '#cbbfa6');
skin.panel(L, x, cy, 150, 110, { radius: 12, variant: sel ? 'panel' : 'dark', raster: true, alpha: sel ? 1 : 0.92 });
// 徽记:职业起始遗物真图(帧就绪)/ 程序灵珠兜底——每张卡都有一件像样的美术件,不再是纯文字黑块。
const relicId = CLASSES[c.id] && CLASSES[c.id].starterRelic;
const rk = relicId ? frames.atlasOf(relicId) : null;
if (rk) keep(scene, scene.add.image(x, cy - 26, rk, relicId).setDisplaySize(48, 48).setDepth(11));
else skin.orb(L, x, cy - 26, 20);
stxt(scene, x, cy + 12, c.name, 17, sel ? INK_TEXT : GOLD_TEXT);
stxt(scene, x, cy + 31, `气血 ${c.hpMax}`, 11, sel ? '#5a4a30' : '#9fc4e8');
stxt(scene, x, cy + 46, c.id === 'jia' ? '攻伐见长' : '灵脉见长', 10, sel ? '#6a5636' : '#cbbfa6');
if (sel) {
const fr = keep(scene, scene.add.graphics().setDepth(11));
fr.lineStyle(3, GOLD, 1); fr.strokeRoundedRect(x - 75, cy - 55, 150, 110, 12);
fr.lineStyle(1.2, ZHUSHA, 0.85); fr.strokeRoundedRect(x - 78, cy - 58, 156, 116, 13);
}
}
// 进阶选择(加减 + 当前级文案)。
// 进阶 stepper(靶 ascMinus/ascPlus 定死 ±110/y0.5):中间加浅米册页承文案,去「两个孤按钮夹一行字」。
skin.panel(L, VIEW_W / 2, VIEW_H * 0.5, 150, 52, { radius: 10 });
skin.textButton(L, VIEW_W / 2 - 110, VIEW_H * 0.5, 52, 44, '', { variant: 'secondary', size: 22 });
skin.textButton(L, VIEW_W / 2 + 110, VIEW_H * 0.5, 52, 44, '+', { variant: 'secondary', size: 22 });
const asc = hs.home.sel.ascension;
stxt(scene, VIEW_W / 2, VIEW_H * 0.48, `进阶 ${asc}${hs.home.maxAscension > 0 ? ` / 已启 ${hs.home.maxAscension}` : ''}`, 16, hx(CIHUANG));
stxt(scene, VIEW_W / 2, VIEW_H * 0.52, asc > 0 ? (hs.home.ascensions[asc - 1] || {}).text || '' : '无修正 · 基准巡途', 11, '#cbbfa6');
// 出巡/每日。
stxt(scene, VIEW_W / 2, VIEW_H * 0.485, `进阶 ${asc}${hs.home.maxAscension > 0 ? ` / 已启 ${hs.home.maxAscension}` : ''}`, 15, INK_TEXT);
keep(scene, scene.add.text(VIEW_W / 2, VIEW_H * 0.515, asc > 0 ? (hs.home.ascensions[asc - 1] || {}).text || '' : '无修正 · 基准巡途', { fontSize: '10px', color: '#5a4a30', fontFamily: 'serif', wordWrap: { width: 138 }, align: 'center' }).setOrigin(0.5).setDepth(12));
// 出巡/每日(靶 homeStart/homeDaily 定死 y0.62/y0.72)
skin.textButton(L, VIEW_W / 2, VIEW_H * 0.62, 240, 56, '启程出巡', { variant: 'primary', size: 20 });
skin.textButton(L, VIEW_W / 2, VIEW_H * 0.72, 240, 48, `每日一巡 · ${hs.home.daily.date}`, { variant: 'jade', size: 14, sub: m.daily.bestScore > 0 ? `本地最佳 ${m.daily.bestScore}` : '固定种子 · 天下同题', subColor: '#cfeee0' });
// 图鉴/成就入口
// 图鉴/成就入口(靶 homeCollection/homeAchievements 定死 x=±90/y0.82)
skin.textButton(L, VIEW_W / 2 - 90, VIEW_H * 0.82, 150, 44, `图鉴 ${m.collection.cards + m.collection.relics + m.collection.enemies}`, { variant: 'secondary', size: 13 });
skin.textButton(L, VIEW_W / 2 + 90, VIEW_H * 0.82, 150, 44, `成就 ${m.achievedCount}/40`, { variant: 'secondary', size: 13 });
}
@ -640,6 +687,10 @@ export function createRunScene(api) {
// ── 事件演出(M0 语汇迁入 + run 级新事件;敌意图以 drawIntent/intentBadge 出徽,不再走纯文字标签)──
function playFx(scene, e) {
// ① 奖励横幅遮挡修复:战斗结算类演出(伤害/爆字/链环/敌我动作/顶栏金字…)仅在战斗屏出。
// 击杀翻入战利屏后,这批延迟触发的飘字会压住题头匾额(战利·三选一);离战屏即整条抑制(音画同抑)。
// 过场/奖励/商店类(得遗物/收入卡组/幕启/入市…)不在 BATTLE_ONLY_FX 内,照常播。
if (BATTLE_ONLY_FX.has(e.t) && !onBattleScreen()) return;
// 音画同帧(§7.2):同一事件在此同步出音+画。链环走八级音阶递升(chainStep),余按 sfxForEvent 映射。
let hadAudio = false;
if (e.t === 'chainLink') { sfx.chainStep(e.links); hadAudio = true; }

View File

@ -397,8 +397,17 @@ export function cardFront(L, cx, cy, card, opt, CARD_W, CARD_H, elemColors) {
cg.lineStyle(1.6, GOLD, 0.95); cg.strokeCircle(-hw + 12, -hh + 12, 11);
cont.add(cg);
cont.add(scene.add.text(-hw + 12, -hh + 12, String(card.cost), { fontSize: '13px', color: '#fff', fontFamily: 'serif', fontStyle: 'bold' }).setOrigin(0.5));
// 文案(下半;浅米纸面墨字)
cont.add(scene.add.text(0, hh - 30, card.text || '', { fontSize: '10px', color: fk ? INK_TEXT : '#3a2c18', fontFamily: 'sans-serif', wordWrap: { width: CARD_W - 16 }, align: 'center' }).setOrigin(0.5));
// 下半排版(缺点④「卡面下半空白」修复):原实现只在卡底居中放一行文案,插画与文案之间大片浅米纸面
// 悬空,短文案更显未完成。现自插画下沿起顺次填满——元素色分隔线 → 顶对齐描述文案 → 底部元素/类型脚注。
const bodyTop = -hh + 88; // 插画下沿(-hh+84 一线)再让 4px
const dv = scene.add.graphics(); // 元素色描金分隔线,划出「插画 / 说明」两段
dv.lineStyle(1, ecol, 0.7); dv.lineBetween(-hw + 12, bodyTop, hw - 12, bodyTop);
dv.lineStyle(1, GOLD_DK, 0.5); dv.lineBetween(-hw + 12, bodyTop + 1.6, hw - 12, bodyTop + 1.6);
cont.add(dv);
// 描述文案:顶对齐自分隔线下起、向下铺满(短文案不再悬空居中;行距收紧,最长三行卡也与脚注留净空)。
cont.add(scene.add.text(0, bodyTop + 4, card.text || '', { fontSize: '10px', lineSpacing: 1, color: fk ? INK_TEXT : '#3a2c18', fontFamily: 'sans-serif', wordWrap: { width: CARD_W - 14 }, align: 'center' }).setOrigin(0.5, 0));
// 脚注:元素 · 类型(费用已在左上费珠;此处补「数值排版」并锚住卡底,填满下缘)。
cont.add(scene.add.text(0, hh - 9, `${card.element} · ${card.type}`, { fontSize: '9px', color: hx(ecol), fontFamily: 'serif', fontStyle: 'bold' }).setOrigin(0.5));
// 选中态:朱砂描金外框 + 抬亮
if (opt.selected) {
const sg = scene.add.graphics();