v0.1.33c: UI/动效打磨——过渡动画/停帧节能/季节对齐/反馈闭环
【动效】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 全绿
This commit is contained in:
@@ -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 (
|
||||
<div className="drift-layer">
|
||||
{items.map((it) => (
|
||||
<span key={it.id} className={`drift ${it.cls}`}>{it.text}</span>
|
||||
<span key={it.id} className={`drift ${it.cls}`} onAnimationEnd={() => setItems((old) => old.filter((x) => x.id !== it.id))}>{it.text}</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<string, string> = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', stones: '灵石' }
|
||||
const RES_NAME: Record<string, string> = {
|
||||
lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', stones: '灵石',
|
||||
lingyu: '灵玉', lingmu: '灵木', shoupi: '兽皮', lingguo: '灵果', linglu: '灵露', dansha: '丹砂', fuzhi: '符纸',
|
||||
'talisman-feng': '风符', 'talisman-shan': '山符', 'brew-niang': '灵酿'
|
||||
}
|
||||
|
||||
function describeEff(eff: Record<string, never> | { [k: string]: unknown }): string {
|
||||
const parts: string[] = []
|
||||
@@ -23,7 +28,7 @@ function describeEff(eff: Record<string, never> | { [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}`)
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
</b></div>
|
||||
<div className="mm-line"><span>气血</span><b>{Math.round(member.health)}</b></div>
|
||||
<div className="mm-line"><span>功法</span><b>{tech ? `${tech.name}(${gradeShort(tech.grade)}阶)` : member.realm.major === 'mortal' ? '—' : '未习功法'}</b></div>
|
||||
<div className="mm-line"><span>法宝</span><b>{member.equipment ? `${ITEMS[member.equipment].name}(+${Math.round((ARTIFACT_POWER[member.equipment] ?? 0) * 100)}%)` : '—'}</b></div>
|
||||
<div className="mm-line"><span>法宝</span><b>{member.equipment ? `${ITEMS[member.equipment]?.name ?? '未知名器'}(+${Math.round((ARTIFACT_POWER[member.equipment] ?? 0) * 100)}%)` : '—'}</b></div>
|
||||
<div className="mm-line"><span>姻亲</span><b>{member.spouseId ? s.members[member.spouseId]?.name ?? '' : member.spouseHouse ?? '未婚'}</b></div>
|
||||
</div>
|
||||
|
||||
@@ -284,7 +284,7 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
<button
|
||||
key={pid}
|
||||
className={`btn btn-sm ${member.post === pid ? 'btn-primary' : ''}`}
|
||||
disabled={maxed && !!member.post}
|
||||
disabled={maxed}
|
||||
style={maxed && !member.post ? { opacity: 0.45 } : undefined}
|
||||
title={`${def.desc}${w.postCount(pid)}/${def.max} 在任`}
|
||||
onClick={() => {
|
||||
|
||||
@@ -53,7 +53,9 @@ function resize(): void {
|
||||
}
|
||||
|
||||
function seasonColor(): string {
|
||||
const s = seasonOf(new Date().getMonth() + 1) as 'spring' | 'summer' | 'autumn' | 'winter'
|
||||
const attr = typeof document !== 'undefined' ? document.documentElement.getAttribute('data-season') : null
|
||||
const gameSeason = (['spring', 'summer', 'autumn', 'winter'] as const).includes(attr as never) ? attr : seasonOf(new Date().getMonth() + 1)
|
||||
const s = gameSeason as 'spring' | 'summer' | 'autumn' | 'winter'
|
||||
return ({ spring: '154,180,130', summer: '224,189,84', autumn: '224,148,74', winter: '138,167,189' } as Record<string, string>)[s] ?? '216,182,120'
|
||||
}
|
||||
|
||||
@@ -147,7 +149,11 @@ export function stopFxLayer(): void {
|
||||
}
|
||||
|
||||
export function spawnBurst(kind: 'spark' | 'blade' | 'ripple', origin?: string): void {
|
||||
if (!running) return
|
||||
if (!running) {
|
||||
// 从空态唤醒(0.1.33:待机停帧后,事件到来即重启)
|
||||
running = true
|
||||
raf = window.requestAnimationFrame(frame as FrameRequestCallback)
|
||||
}
|
||||
if (kind === 'spark') {
|
||||
const X = window.innerWidth / 2
|
||||
const Y = window.innerHeight * (origin === 'center' ? 0.5 : 0.3)
|
||||
|
||||
@@ -33,7 +33,7 @@ export class FxGate {
|
||||
allowed(kind: FxKind): boolean {
|
||||
if (this.mode === 'soft') return kind === 'ripple' || kind === 'pulse' || kind === 'drift'
|
||||
if (this.mode === 'full') return true
|
||||
return kind !== 'blade' // std:火花/雾/涟漪/脉动,刀光仅 full
|
||||
return true // 0.1.33 std 亦放行刀光(战场核心反馈,full 档仍全量)
|
||||
}
|
||||
|
||||
emit(kind: FxKind, payload?: FxEmit['payload']): void {
|
||||
|
||||
@@ -84,7 +84,12 @@ export default function DiplomacyPanel() {
|
||||
寻衅
|
||||
</button>
|
||||
{npc.relation < -30 && (
|
||||
<button className="btn btn-sm" onClick={() => { makePeace(w, npc.id); bump() }}>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={w.state.family.stones < 200}
|
||||
title={w.state.family.stones < 200 ? '需灵石 200' : '与对方结下和约'}
|
||||
onClick={() => { makePeace(w, npc.id); bump() }}
|
||||
>
|
||||
议和
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<tr key={item.id}>
|
||||
|
||||
@@ -77,9 +77,9 @@ export default function GameScreen() {
|
||||
</div>
|
||||
<div className="speed-ctl">
|
||||
<button className={`speed-btn ${speed === 0 ? 'sel' : ''}`} title="暂停时间" onClick={() => setSpeed(0)}>止</button>
|
||||
<button className={`speed-btn ${speed === 1 ? 'gel' : ''}`} title="缓行(约3月/秒)" onClick={() => setSpeed(1)}>缓</button>
|
||||
<button className={`speed-btn ${speed === 2 ? 'gel' : ''}`} title="常速(约半秒/月)" onClick={() => setSpeed(2)}>速</button>
|
||||
<button className={`speed-btn ${speed === 3 ? 'gel' : ''}`} title="疾进(约1月/秒)" onClick={() => setSpeed(3)}>疾</button>
|
||||
<button className={`speed-btn ${speed === 1 ? 'sel' : ''}`} title="缓行(约3月/秒)" onClick={() => setSpeed(1)}>缓</button>
|
||||
<button className={`speed-btn ${speed === 2 ? 'sel' : ''}`} title="常速(约半秒/月)" onClick={() => setSpeed(2)}>速</button>
|
||||
<button className={`speed-btn ${speed === 3 ? 'sel' : ''}`} title="疾进(约1月/秒)" onClick={() => setSpeed(3)}>疾</button>
|
||||
<button className="speed-btn tick-now" title="推进一月" onClick={() => void advance()}>月</button>
|
||||
</div>
|
||||
<div className="ress">
|
||||
@@ -125,7 +125,7 @@ export default function GameScreen() {
|
||||
家族于 {s.year} 年 {s.month} 月戛然而止。可在史书中凭吊这家百年。
|
||||
</p>
|
||||
<div style={{ marginTop: 20, display: 'flex', gap: 12, justifyContent: 'center' }}>
|
||||
<button className="btn btn-primary" onClick={() => void useGameStore.getState().refreshSnapshots().then(() => {
|
||||
<button className="btn btn-primary" disabled={(!useGameStore.getState().snapshots?.length)} title={useGameStore.getState().snapshots?.length ? '回到最近存点' : '暂无可用存点——可在设置页手动存档'} onClick={() => void useGameStore.getState().refreshSnapshots().then(() => {
|
||||
const snaps = useGameStore.getState().snapshots
|
||||
if (snaps[0]) void useGameStore.getState().restoreSnapshot(snaps[0].id)
|
||||
})}>回卷到最近存点</button>
|
||||
|
||||
@@ -174,6 +174,9 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
set({ slots })
|
||||
if (typeof window !== 'undefined') {
|
||||
;(window as unknown as Record<string, unknown>).__cotycBootReady = true
|
||||
// 0.1.33:调试后门仅开发/冒烟挂载(生产干净)
|
||||
const smoke = typeof (window as unknown as Record<string, unknown>).__SMOKE__ === 'boolean' ? (window as unknown as Record<string, unknown>).__SMOKE__ as boolean : import.meta.env.DEV
|
||||
if (smoke) {
|
||||
const hack = window as unknown as Record<string, unknown>
|
||||
hack.__cotycDebug = {
|
||||
startNewGame: () => get().startNewGame({ seed: 'smoke-' + Date.now(), surname: '林', familyName: '林氏', motto: 'm', difficulty: 'normal' }, 1),
|
||||
@@ -196,6 +199,7 @@ export const useGameStore = create<GameStore>((set, get) => ({
|
||||
getChronicle: () => get().world?.state.chronicle?.slice().reverse() ?? [],
|
||||
getFeed: () => get().logFeed.slice()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+27
-12
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user