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:
2026-08-23 21:38:02 +08:00
parent 3e4ed4d882
commit 2369f342dd
13 changed files with 68 additions and 37 deletions
+1
View File
@@ -63,6 +63,7 @@ function createWindow(): void {
const title = await wc.executeJavaScript('document.title') const title = await wc.executeJavaScript('document.title')
let bootReady = 0 let bootReady = 0
for (let i = 0; i < 40; i++) { for (let i = 0; i < 40; i++) {
await wc.executeJavaScript('window.__SMOKE__ = true; void 0')
bootReady = await wc.executeJavaScript( bootReady = await wc.executeJavaScript(
"Number(window.__cotycBootReady ?? 0)" "Number(window.__cotycBootReady ?? 0)"
) )
@@ -124,6 +124,7 @@ export function resolveEncounter(
c.deathCause = `战殁于${opts.enemy.name}之手` c.deathCause = `战殁于${opts.enemy.name}之手`
loss.push(`${c.name} 陨落`) loss.push(`${c.name} 陨落`)
w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true) w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true)
w.emitFx('ripple', `death:${c.id}`)
} else if (severity < 0.45) { } else if (severity < 0.45) {
const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound * (talisman === 'shan' ? 0.5 : 1)) 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) c.health = Math.max(1, c.health - woundAmt)
+5 -9
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' 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 条自动修剪尾部 */ /** 资源漂字层:每次 fx:drift 事件追加独立节点(互不覆盖),超 8 条自动修剪尾部 */
export function DriftLayer() { export function DriftLayer() {
@@ -12,22 +12,18 @@ export function DriftLayer() {
const next: DriftItem[] = [] const next: DriftItem[] = []
for (const [k, v] of Object.entries(detail)) { for (const [k, v] of Object.entries(detail)) {
if (v === 0 || !Number.isFinite(v)) continue 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 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) window.addEventListener('fx:drift', onDrift)
const timer = setInterval(() => setItems((old) => old.slice(0)), 2600) // 到期清理由 CSS fade 承担 return () => window.removeEventListener('fx:drift', onDrift)
return () => {
window.removeEventListener('fx:drift', onDrift)
clearInterval(timer)
}
}, []) }, [])
return ( return (
<div className="drift-layer"> <div className="drift-layer">
{items.map((it) => ( {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> </div>
) )
+7 -2
View File
@@ -6,9 +6,14 @@ import { describeRealm } from '../../game/data/realms'
import { Character } from '../../game/types/domain' import { Character } from '../../game/types/domain'
import { combatPowerOf } from '../../game/engine/runtime/Systems/combat' import { combatPowerOf } from '../../game/engine/runtime/Systems/combat'
import { FORMATIONS, FormationId } from '../../game/data/formations' import { FORMATIONS, FormationId } from '../../game/data/formations'
import { buildingById } from '../../game/data/buildings'
import { ModalShell } from './ModalShell' 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 { function describeEff(eff: Record<string, never> | { [k: string]: unknown }): string {
const parts: string[] = [] const parts: string[] = []
@@ -23,7 +28,7 @@ function describeEff(eff: Record<string, never> | { [k: string]: unknown }): str
if (e.relation) { if (e.relation) {
for (const v of Object.values(e.relation)) parts.push(v > 0 ? `交好+${v}` : `交恶${v}`) 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) { if (e.pillGain) {
for (const v of Object.values(e.pillGain)) parts.push(`得丹+${v}`) for (const v of Object.values(e.pillGain)) parts.push(`得丹+${v}`)
} }
+2 -2
View File
@@ -107,7 +107,7 @@ export function MemberModal({ member }: { member: Character }) {
</b></div> </b></div>
<div className="mm-line"><span></span><b>{Math.round(member.health)}</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>{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 className="mm-line"><span></span><b>{member.spouseId ? s.members[member.spouseId]?.name ?? '' : member.spouseHouse ?? '未婚'}</b></div>
</div> </div>
@@ -284,7 +284,7 @@ export function MemberModal({ member }: { member: Character }) {
<button <button
key={pid} key={pid}
className={`btn btn-sm ${member.post === pid ? 'btn-primary' : ''}`} className={`btn btn-sm ${member.post === pid ? 'btn-primary' : ''}`}
disabled={maxed && !!member.post} disabled={maxed}
style={maxed && !member.post ? { opacity: 0.45 } : undefined} style={maxed && !member.post ? { opacity: 0.45 } : undefined}
title={`${def.desc}${w.postCount(pid)}/${def.max} 在任`} title={`${def.desc}${w.postCount(pid)}/${def.max} 在任`}
onClick={() => { onClick={() => {
+8 -2
View File
@@ -53,7 +53,9 @@ function resize(): void {
} }
function seasonColor(): string { 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' 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 { 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') { if (kind === 'spark') {
const X = window.innerWidth / 2 const X = window.innerWidth / 2
const Y = window.innerHeight * (origin === 'center' ? 0.5 : 0.3) const Y = window.innerHeight * (origin === 'center' ? 0.5 : 0.3)
+1 -1
View File
@@ -33,7 +33,7 @@ export class FxGate {
allowed(kind: FxKind): boolean { allowed(kind: FxKind): boolean {
if (this.mode === 'soft') return kind === 'ripple' || kind === 'pulse' || kind === 'drift' if (this.mode === 'soft') return kind === 'ripple' || kind === 'pulse' || kind === 'drift'
if (this.mode === 'full') return true 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 { emit(kind: FxKind, payload?: FxEmit['payload']): void {
+6 -1
View File
@@ -84,7 +84,12 @@ export default function DiplomacyPanel() {
</button> </button>
{npc.relation < -30 && ( {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> </button>
)} )}
-1
View File
@@ -56,7 +56,6 @@ export default function MarketPanel() {
const have = ['resource', 'material', 'pill', 'talisman', 'brew', 'artifact'].includes(item.kind) const have = ['resource', 'material', 'pill', 'talisman', 'brew', 'artifact'].includes(item.kind)
? inv[item.id] ?? 0 ? inv[item.id] ?? 0
: 0 : 0
const stock = item.kind === 'artifact' ? 3 : ['pill', 'talisman', 'brew'].includes(item.kind) ? 5 : 9999
const count = item.kind === 'artifact' ? 1 : 5 const count = item.kind === 'artifact' ? 1 : 5
return ( return (
<tr key={item.id}> <tr key={item.id}>
+4 -4
View File
@@ -77,9 +77,9 @@ export default function GameScreen() {
</div> </div>
<div className="speed-ctl"> <div className="speed-ctl">
<button className={`speed-btn ${speed === 0 ? 'sel' : ''}`} title="暂停时间" onClick={() => setSpeed(0)}></button> <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 === 1 ? 'sel' : ''}`} title="缓行(约3月/秒)" onClick={() => setSpeed(1)}></button>
<button className={`speed-btn ${speed === 2 ? 'gel' : ''}`} title="常速(约半秒/月)" onClick={() => setSpeed(2)}></button> <button className={`speed-btn ${speed === 2 ? 'sel' : ''}`} title="常速(约半秒/月)" onClick={() => setSpeed(2)}></button>
<button className={`speed-btn ${speed === 3 ? 'gel' : ''}`} title="疾进(约1月/秒)" onClick={() => setSpeed(3)}></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> <button className="speed-btn tick-now" title="推进一月" onClick={() => void advance()}></button>
</div> </div>
<div className="ress"> <div className="ress">
@@ -125,7 +125,7 @@ export default function GameScreen() {
{s.year} {s.month} {s.year} {s.month}
</p> </p>
<div style={{ marginTop: 20, display: 'flex', gap: 12, justifyContent: 'center' }}> <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 const snaps = useGameStore.getState().snapshots
if (snaps[0]) void useGameStore.getState().restoreSnapshot(snaps[0].id) if (snaps[0]) void useGameStore.getState().restoreSnapshot(snaps[0].id)
})}></button> })}></button>
+4
View File
@@ -174,6 +174,9 @@ export const useGameStore = create<GameStore>((set, get) => ({
set({ slots }) set({ slots })
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
;(window as unknown as Record<string, unknown>).__cotycBootReady = true ;(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> const hack = window as unknown as Record<string, unknown>
hack.__cotycDebug = { hack.__cotycDebug = {
startNewGame: () => get().startNewGame({ seed: 'smoke-' + Date.now(), surname: '林', familyName: '林氏', motto: 'm', difficulty: 'normal' }, 1), 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() ?? [], getChronicle: () => get().world?.state.chronicle?.slice().reverse() ?? [],
getFeed: () => get().logFeed.slice() getFeed: () => get().logFeed.slice()
} }
}
} }
}, },
+27 -12
View File
@@ -216,7 +216,7 @@ html[data-blade] .battle-lines {
font-family: inherit; font-family: inherit;
font-size: 0.95rem; font-size: 0.95rem;
letter-spacing: 1px; 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); 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) { .btn:hover:not(:disabled) {
@@ -512,7 +512,7 @@ html[data-blade] .battle-lines {
font-family: inherit; font-family: inherit;
text-shadow: 0 1px 2px rgba(70, 15, 8, 0.65); 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); 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 { .boot-newgame:hover {
box-shadow: 0 10px 32px rgba(180, 60, 35, 0.5), inset 0 1px 0 rgba(255, 240, 210, 0.3); 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; border-radius: 3px;
padding: 10px 14px; padding: 10px 14px;
cursor: pointer; 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); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
} }
.boot-slot:hover { .boot-slot:hover {
@@ -623,7 +623,7 @@ html[data-blade] .battle-lines {
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
color: #ddceac; color: #ddceac;
transition: all 0.16s; transition: background-color, color, transform, opacity, border-color 0.16s;
} }
.diff-opt:hover { .diff-opt:hover {
border-color: #6b5836; border-color: #6b5836;
@@ -703,7 +703,7 @@ html[data-blade] .battle-lines {
font-family: inherit; font-family: inherit;
border-radius: 3px; border-radius: 3px;
box-shadow: inset 0 1px 0 rgba(255, 240, 200, 0.06), 0 2px 5px rgba(0, 0, 0, 0.45); 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; user-select: none;
} }
.speed-btn:hover { .speed-btn:hover {
@@ -835,7 +835,7 @@ html[data-blade] .battle-lines {
border-radius: 3px; border-radius: 3px;
padding: 12px 12px 10px; padding: 12px 12px 10px;
cursor: pointer; cursor: pointer;
transition: all 0.18s; transition: background-color, color, transform, opacity, border-color 0.18s;
box-shadow: box-shadow:
0 4px 12px rgba(0, 0, 0, 0.55), 0 4px 12px rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(255, 252, 242, 0.6), inset 0 0 0 1px rgba(255, 252, 242, 0.6),
@@ -939,6 +939,7 @@ html[data-blade] .battle-lines {
flex: 1; flex: 1;
} }
.feed-item { .feed-item {
animation: feedIn 0.3s ease both;
padding: 5px 8px; padding: 5px 8px;
border-left: 2px solid transparent; border-left: 2px solid transparent;
margin-bottom: 3px; margin-bottom: 3px;
@@ -1164,7 +1165,7 @@ html[data-blade] .battle-lines {
font-size: 0.85rem; font-size: 0.85rem;
color: #64502e; color: #64502e;
background: #f7f0de; background: #f7f0de;
transition: all 0.14s; transition: background-color, color, transform, opacity, border-color 0.14s;
} }
.squadian:hover { .squadian:hover {
border-color: var(--gold); 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 { .modal-outer {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(5, 4, 2, 0.82); background: rgba(5, 4, 2, 0.82);
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
animation: modalFadeIn 0.22s ease both;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1244,6 +1258,7 @@ html[data-blade] .battle-lines {
position: relative; position: relative;
width: 570px; width: 570px;
max-height: 82vh; max-height: 82vh;
animation: modalRise 0.26s cubic-bezier(0.2, 0.8, 0.3, 1) both;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
@@ -1324,7 +1339,7 @@ html[data-blade] .battle-lines {
align-items: center; align-items: center;
justify-content: 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); 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 { .modal-close:hover {
border-color: var(--vermilion); border-color: var(--vermilion);
@@ -1394,7 +1409,7 @@ html[data-blade] .battle-lines {
font-family: inherit; font-family: inherit;
color: #342a18; color: #342a18;
font-size: 0.98rem; 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); box-shadow: inset 0 1px 0 rgba(255, 255, 250, 0.6);
} }
.opt-btn:hover { .opt-btn:hover {
@@ -1536,7 +1551,7 @@ html[data-blade] .battle-lines {
min-width: 108px; min-width: 108px;
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 252, 240, 0.6); 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 { .gene-node:hover {
border-color: var(--gold); border-color: var(--gold);
@@ -1843,7 +1858,7 @@ html[data-blade] .battle-lines {
cursor: pointer; cursor: pointer;
font-family: inherit; font-family: inherit;
letter-spacing: 1px; letter-spacing: 1px;
transition: all 0.14s; transition: background-color, color, transform, opacity, border-color 0.14s;
} }
.badge:hover { .badge:hover {
transform: translateY(-1px); transform: translateY(-1px);
@@ -1886,7 +1901,7 @@ html[data-blade] .battle-lines {
cursor: pointer; cursor: pointer;
font-family: inherit; font-family: inherit;
color: #ddceac; color: #ddceac;
transition: all 0.15s; transition: background-color, color, transform, opacity, border-color 0.15s;
} }
.world-badge:hover { .world-badge:hover {
border-color: var(--gold); border-color: var(--gold);
+2 -3
View File
@@ -40,15 +40,14 @@ describe('FxGate 竞态安全', () => {
return { gate, emitted } return { gate, emitted }
} }
it('std 模式:spark/mist/ripple/pulse 允许,blade 需 full', () => { it('std 模式:全量允许(0.1.33 刀光下放)', () => {
const { gate, emitted } = freshGate() const { gate, emitted } = freshGate()
gate.emit('spark') gate.emit('spark')
gate.emit('mist') gate.emit('mist')
gate.emit('ripple') gate.emit('ripple')
gate.emit('pulse') gate.emit('pulse')
gate.emit('blade') gate.emit('blade')
expect(emitted).toEqual(['spark', 'mist', 'ripple', 'pulse']) expect(emitted).toEqual(['spark', 'mist', 'ripple', 'pulse', 'blade'])
expect(emitted).not.toContain('blade')
}) })
it('soft 模式:仅 ripple/pulse/drift', () => { it('soft 模式:仅 ripple/pulse/drift', () => {