diff --git a/tier2/games/shanhai-xunyilu/src/battle-core.js b/tier2/games/shanhai-xunyilu/src/battle-core.js index 99de1c10..191677cf 100644 --- a/tier2/games/shanhai-xunyilu/src/battle-core.js +++ b/tier2/games/shanhai-xunyilu/src/battle-core.js @@ -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, diff --git a/tier2/games/shanhai-xunyilu/src/scenes/run-scene.js b/tier2/games/shanhai-xunyilu/src/scenes/run-scene.js index fb43aace..ba462adb 100644 --- a/tier2/games/shanhai-xunyilu/src/scenes/run-scene.js +++ b/tier2/games/shanhai-xunyilu/src/scenes/run-scene.js @@ -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; } diff --git a/tier2/games/shanhai-xunyilu/src/ui/skin.js b/tier2/games/shanhai-xunyilu/src/ui/skin.js index 3d7342c5..54b18683 100644 --- a/tier2/games/shanhai-xunyilu/src/ui/skin.js +++ b/tier2/games/shanhai-xunyilu/src/ui/skin.js @@ -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();