From 2369f342dd58178f78f9d7eb25e01f4077154a86 Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Sun, 23 Aug 2026 21:38:02 +0800 Subject: [PATCH] =?UTF-8?q?v0.1.33c:=20UI/=E5=8A=A8=E6=95=88=E6=89=93?= =?UTF-8?q?=E7=A3=A8=E2=80=94=E2=80=94=E8=BF=87=E6=B8=A1=E5=8A=A8=E7=94=BB?= =?UTF-8?q?/=E5=81=9C=E5=B8=A7=E8=8A=82=E8=83=BD/=E5=AD=A3=E8=8A=82?= =?UTF-8?q?=E5=AF=B9=E9=BD=90/=E5=8F=8D=E9=A6=88=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【动效】Modal 淡入+升起缓动(modalFadeIn/modalRise);LogFeed 入场动画(feedIn); fx std 档放行刀光(战场核心反馈);任务凯旋补 spark/战殁补 ripple 【节能】DriftLayer 2.6s 转 onAnimationEnd 删除节点(消除空转 setInterval×slice 定期 setState); fx-canvas 粒子空态停帧(空 rAF 取消+spawnBurst 唤醒)——低端机待机零开销 【季节】fx-canvas 季节色改读 data-season(游戏月驱动,不再用系统月——夏季粒子不再飘冬季色) 【性能】transition all 12 处→具体属性(background-color/color/transform/opacity); backdrop-filter 保留(视觉主) 【UI 修复】MemberModal 装备栏空守卫(未知名器)+职事按钮 disabled={maxed} 统一; 议和按钮灵石不足 disabled+title(不再静默);事件弹窗 RES_NAME 补新材料/增筑显示中文名; 回卷按钮无存点 disabled+title;速度按钮 sel/gel 类名统一;MarketPanel 删死变量 stock; debug 后门 __SMOKE__/DEV 门控(生产干净,冒烟仍可用——main 注入 __SMOKE__) 【测试】fx-timesense std 刀光断言更新 5030 全绿 --- src/main/index.ts | 1 + .../game/engine/runtime/Systems/combat.ts | 1 + src/renderer/ui/components/DriftLayer.tsx | 14 +++---- src/renderer/ui/components/EventModal.tsx | 9 ++++- src/renderer/ui/components/MemberModal.tsx | 4 +- src/renderer/ui/fx-canvas.ts | 10 ++++- src/renderer/ui/fx.ts | 2 +- src/renderer/ui/panels/DiplomacyPanel.tsx | 7 +++- src/renderer/ui/panels/MarketPanel.tsx | 1 - src/renderer/ui/screens/GameScreen.tsx | 8 ++-- src/renderer/ui/store.ts | 4 ++ src/renderer/ui/styles.css | 39 +++++++++++++------ tests/fx-timesense.test.ts | 5 +-- 13 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 0b0f024..b2f0fcb 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -63,6 +63,7 @@ function createWindow(): void { const title = await wc.executeJavaScript('document.title') let bootReady = 0 for (let i = 0; i < 40; i++) { + await wc.executeJavaScript('window.__SMOKE__ = true; void 0') bootReady = await wc.executeJavaScript( "Number(window.__cotycBootReady ?? 0)" ) diff --git a/src/renderer/game/engine/runtime/Systems/combat.ts b/src/renderer/game/engine/runtime/Systems/combat.ts index 5a0ba19..13bcd43 100644 --- a/src/renderer/game/engine/runtime/Systems/combat.ts +++ b/src/renderer/game/engine/runtime/Systems/combat.ts @@ -124,6 +124,7 @@ export function resolveEncounter( c.deathCause = `战殁于${opts.enemy.name}之手` loss.push(`${c.name} 陨落`) w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true) + w.emitFx('ripple', `death:${c.id}`) } else if (severity < 0.45) { const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound * (talisman === 'shan' ? 0.5 : 1)) c.health = Math.max(1, c.health - woundAmt) diff --git a/src/renderer/ui/components/DriftLayer.tsx b/src/renderer/ui/components/DriftLayer.tsx index f87dbfe..f753c8a 100644 --- a/src/renderer/ui/components/DriftLayer.tsx +++ b/src/renderer/ui/components/DriftLayer.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' -interface DriftItem { id: number; text: string; cls: string } +interface DriftItem { id: number; text: string; cls: string; at: number } /** 资源漂字层:每次 fx:drift 事件追加独立节点(互不覆盖),超 8 条自动修剪尾部 */ export function DriftLayer() { @@ -12,22 +12,18 @@ export function DriftLayer() { const next: DriftItem[] = [] for (const [k, v] of Object.entries(detail)) { if (v === 0 || !Number.isFinite(v)) continue - next.push({ id: now + Math.random() + k.length, text: `${v > 0 ? '+' : ''}${v}`, cls: v > 0 ? 'pos' : 'neg' }) + next.push({ id: now + Math.random() + k.length, text: `${v > 0 ? '+' : ''}${v}`, cls: v > 0 ? 'pos' : 'neg', at: now }) } if (next.length === 0) return - setItems((old) => [...old, ...next].slice(-8)) + setItems((old) => (next.length + old.length > 8 ? [] : old).concat(next)) } window.addEventListener('fx:drift', onDrift) - const timer = setInterval(() => setItems((old) => old.slice(0)), 2600) // 到期清理由 CSS fade 承担 - return () => { - window.removeEventListener('fx:drift', onDrift) - clearInterval(timer) - } + return () => window.removeEventListener('fx:drift', onDrift) }, []) return (
{items.map((it) => ( - {it.text} + setItems((old) => old.filter((x) => x.id !== it.id))}>{it.text} ))}
) diff --git a/src/renderer/ui/components/EventModal.tsx b/src/renderer/ui/components/EventModal.tsx index b1b800e..2970b3b 100644 --- a/src/renderer/ui/components/EventModal.tsx +++ b/src/renderer/ui/components/EventModal.tsx @@ -6,9 +6,14 @@ import { describeRealm } from '../../game/data/realms' import { Character } from '../../game/types/domain' import { combatPowerOf } from '../../game/engine/runtime/Systems/combat' import { FORMATIONS, FormationId } from '../../game/data/formations' +import { buildingById } from '../../game/data/buildings' import { ModalShell } from './ModalShell' -const RES_NAME: Record = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', stones: '灵石' } +const RES_NAME: Record = { + lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', stones: '灵石', + lingyu: '灵玉', lingmu: '灵木', shoupi: '兽皮', lingguo: '灵果', linglu: '灵露', dansha: '丹砂', fuzhi: '符纸', + 'talisman-feng': '风符', 'talisman-shan': '山符', 'brew-niang': '灵酿' +} function describeEff(eff: Record | { [k: string]: unknown }): string { const parts: string[] = [] @@ -23,7 +28,7 @@ function describeEff(eff: Record | { [k: string]: unknown }): str if (e.relation) { for (const v of Object.values(e.relation)) parts.push(v > 0 ? `交好+${v}` : `交恶${v}`) } - if (e.addBuilding) parts.push('增筑' + e.addBuilding) + if (e.addBuilding) parts.push('增筑「' + (buildingById(e.addBuilding)?.name ?? e.addBuilding) + '」') if (e.pillGain) { for (const v of Object.values(e.pillGain)) parts.push(`得丹+${v}`) } diff --git a/src/renderer/ui/components/MemberModal.tsx b/src/renderer/ui/components/MemberModal.tsx index 482320a..058f7d6 100644 --- a/src/renderer/ui/components/MemberModal.tsx +++ b/src/renderer/ui/components/MemberModal.tsx @@ -107,7 +107,7 @@ export function MemberModal({ member }: { member: Character }) {
气血{Math.round(member.health)}
功法{tech ? `${tech.name}(${gradeShort(tech.grade)}阶)` : member.realm.major === 'mortal' ? '—' : '未习功法'}
-
法宝{member.equipment ? `${ITEMS[member.equipment].name}(+${Math.round((ARTIFACT_POWER[member.equipment] ?? 0) * 100)}%)` : '—'}
+
法宝{member.equipment ? `${ITEMS[member.equipment]?.name ?? '未知名器'}(+${Math.round((ARTIFACT_POWER[member.equipment] ?? 0) * 100)}%)` : '—'}
姻亲{member.spouseId ? s.members[member.spouseId]?.name ?? '' : member.spouseHouse ?? '未婚'}
@@ -284,7 +284,7 @@ export function MemberModal({ member }: { member: Character }) { {npc.relation < -30 && ( - )} diff --git a/src/renderer/ui/panels/MarketPanel.tsx b/src/renderer/ui/panels/MarketPanel.tsx index 7425023..a84d389 100644 --- a/src/renderer/ui/panels/MarketPanel.tsx +++ b/src/renderer/ui/panels/MarketPanel.tsx @@ -56,7 +56,6 @@ export default function MarketPanel() { const have = ['resource', 'material', 'pill', 'talisman', 'brew', 'artifact'].includes(item.kind) ? inv[item.id] ?? 0 : 0 - const stock = item.kind === 'artifact' ? 3 : ['pill', 'talisman', 'brew'].includes(item.kind) ? 5 : 9999 const count = item.kind === 'artifact' ? 1 : 5 return ( diff --git a/src/renderer/ui/screens/GameScreen.tsx b/src/renderer/ui/screens/GameScreen.tsx index 637d3ab..83abda7 100644 --- a/src/renderer/ui/screens/GameScreen.tsx +++ b/src/renderer/ui/screens/GameScreen.tsx @@ -77,9 +77,9 @@ export default function GameScreen() {
- - - + + +
@@ -125,7 +125,7 @@ export default function GameScreen() { 家族于 {s.year} 年 {s.month} 月戛然而止。可在史书中凭吊这家百年。

- diff --git a/src/renderer/ui/store.ts b/src/renderer/ui/store.ts index a3f45ed..0b39b7e 100644 --- a/src/renderer/ui/store.ts +++ b/src/renderer/ui/store.ts @@ -174,6 +174,9 @@ export const useGameStore = create((set, get) => ({ set({ slots }) if (typeof window !== 'undefined') { ;(window as unknown as Record).__cotycBootReady = true + // 0.1.33:调试后门仅开发/冒烟挂载(生产干净) + const smoke = typeof (window as unknown as Record).__SMOKE__ === 'boolean' ? (window as unknown as Record).__SMOKE__ as boolean : import.meta.env.DEV + if (smoke) { const hack = window as unknown as Record hack.__cotycDebug = { startNewGame: () => get().startNewGame({ seed: 'smoke-' + Date.now(), surname: '林', familyName: '林氏', motto: 'm', difficulty: 'normal' }, 1), @@ -196,6 +199,7 @@ export const useGameStore = create((set, get) => ({ getChronicle: () => get().world?.state.chronicle?.slice().reverse() ?? [], getFeed: () => get().logFeed.slice() } + } } }, diff --git a/src/renderer/ui/styles.css b/src/renderer/ui/styles.css index a6047a9..9bd37e7 100644 --- a/src/renderer/ui/styles.css +++ b/src/renderer/ui/styles.css @@ -216,7 +216,7 @@ html[data-blade] .battle-lines { font-family: inherit; font-size: 0.95rem; letter-spacing: 1px; - transition: all 0.16s ease; + transition: background-color, color, transform, opacity, border-color 0.16s ease; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 236, 190, 0.05); } .btn:hover:not(:disabled) { @@ -512,7 +512,7 @@ html[data-blade] .battle-lines { font-family: inherit; text-shadow: 0 1px 2px rgba(70, 15, 8, 0.65); box-shadow: 0 8px 26px rgba(140, 40, 24, 0.35), inset 0 1px 0 rgba(255, 240, 210, 0.28); - transition: all 0.18s; + transition: background-color, color, transform, opacity, border-color 0.18s; } .boot-newgame:hover { box-shadow: 0 10px 32px rgba(180, 60, 35, 0.5), inset 0 1px 0 rgba(255, 240, 210, 0.3); @@ -530,7 +530,7 @@ html[data-blade] .battle-lines { border-radius: 3px; padding: 10px 14px; cursor: pointer; - transition: all 0.18s; + transition: background-color, color, transform, opacity, border-color 0.18s; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5); } .boot-slot:hover { @@ -623,7 +623,7 @@ html[data-blade] .battle-lines { border-radius: 3px; cursor: pointer; color: #ddceac; - transition: all 0.16s; + transition: background-color, color, transform, opacity, border-color 0.16s; } .diff-opt:hover { border-color: #6b5836; @@ -703,7 +703,7 @@ html[data-blade] .battle-lines { font-family: inherit; border-radius: 3px; box-shadow: inset 0 1px 0 rgba(255, 240, 200, 0.06), 0 2px 5px rgba(0, 0, 0, 0.45); - transition: all 0.15s; + transition: background-color, color, transform, opacity, border-color 0.15s; user-select: none; } .speed-btn:hover { @@ -835,7 +835,7 @@ html[data-blade] .battle-lines { border-radius: 3px; padding: 12px 12px 10px; cursor: pointer; - transition: all 0.18s; + transition: background-color, color, transform, opacity, border-color 0.18s; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.55), inset 0 0 0 1px rgba(255, 252, 242, 0.6), @@ -939,6 +939,7 @@ html[data-blade] .battle-lines { flex: 1; } .feed-item { + animation: feedIn 0.3s ease both; padding: 5px 8px; border-left: 2px solid transparent; margin-bottom: 3px; @@ -1164,7 +1165,7 @@ html[data-blade] .battle-lines { font-size: 0.85rem; color: #64502e; background: #f7f0de; - transition: all 0.14s; + transition: background-color, color, transform, opacity, border-color 0.14s; } .squadian:hover { border-color: var(--gold); @@ -1230,11 +1231,24 @@ html[data-blade] .battle-lines { /* ============================================================ 弹出镸面 ============================================================ */ +@keyframes feedIn { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes modalFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes modalRise { + from { opacity: 0; transform: translateY(14px) scale(0.985); } + to { opacity: 1; transform: translateY(0) scale(1); } +} .modal-outer { position: fixed; inset: 0; background: rgba(5, 4, 2, 0.82); backdrop-filter: blur(2px); + animation: modalFadeIn 0.22s ease both; display: flex; align-items: center; justify-content: center; @@ -1244,6 +1258,7 @@ html[data-blade] .battle-lines { position: relative; width: 570px; max-height: 82vh; + animation: modalRise 0.26s cubic-bezier(0.2, 0.8, 0.3, 1) both; display: flex; flex-direction: column; overflow: hidden; @@ -1324,7 +1339,7 @@ html[data-blade] .battle-lines { align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,240,200,0.15); - transition: all 0.16s; + transition: background-color, color, transform, opacity, border-color 0.16s; } .modal-close:hover { border-color: var(--vermilion); @@ -1394,7 +1409,7 @@ html[data-blade] .battle-lines { font-family: inherit; color: #342a18; font-size: 0.98rem; - transition: all 0.15s; + transition: background-color, color, transform, opacity, border-color 0.15s; box-shadow: inset 0 1px 0 rgba(255, 255, 250, 0.6); } .opt-btn:hover { @@ -1536,7 +1551,7 @@ html[data-blade] .battle-lines { min-width: 108px; cursor: pointer; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 252, 240, 0.6); - transition: all 0.14s; + transition: background-color, color, transform, opacity, border-color 0.14s; } .gene-node:hover { border-color: var(--gold); @@ -1843,7 +1858,7 @@ html[data-blade] .battle-lines { cursor: pointer; font-family: inherit; letter-spacing: 1px; - transition: all 0.14s; + transition: background-color, color, transform, opacity, border-color 0.14s; } .badge:hover { transform: translateY(-1px); @@ -1886,7 +1901,7 @@ html[data-blade] .battle-lines { cursor: pointer; font-family: inherit; color: #ddceac; - transition: all 0.15s; + transition: background-color, color, transform, opacity, border-color 0.15s; } .world-badge:hover { border-color: var(--gold); diff --git a/tests/fx-timesense.test.ts b/tests/fx-timesense.test.ts index 0b6e270..5ca57b0 100644 --- a/tests/fx-timesense.test.ts +++ b/tests/fx-timesense.test.ts @@ -40,15 +40,14 @@ describe('FxGate 竞态安全', () => { return { gate, emitted } } - it('std 模式:spark/mist/ripple/pulse 允许,blade 需 full', () => { + it('std 模式:全量允许(0.1.33 刀光下放)', () => { const { gate, emitted } = freshGate() gate.emit('spark') gate.emit('mist') gate.emit('ripple') gate.emit('pulse') gate.emit('blade') - expect(emitted).toEqual(['spark', 'mist', 'ripple', 'pulse']) - expect(emitted).not.toContain('blade') + expect(emitted).toEqual(['spark', 'mist', 'ripple', 'pulse', 'blade']) }) it('soft 模式:仅 ripple/pulse/drift', () => {