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', () => {