【动效】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 全绿
180 lines
7.3 KiB
TypeScript
180 lines
7.3 KiB
TypeScript
import { useState } from 'react'
|
|
import { useGameStore } from '../store'
|
|
import { applyEventChoice, rankPower } from '../../game/engine/runtime/Systems/events'
|
|
import { eventCategoryName } from '../../game/data/events'
|
|
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: '灵石',
|
|
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[] = []
|
|
const e = eff as { res?: Record<string, number>; rep?: number; relation?: Record<string, number>; addBuilding?: string; pillGain?: Record<string, number>; mission?: string; addTech?: string; raid?: { npcId: string }; trib?: { mode: string }; feisheng?: { stay: boolean }; techniqueChance?: number; artifactChance?: number; flag?: Record<string, unknown> }
|
|
if (e.res) {
|
|
for (const [k, v] of Object.entries(e.res)) {
|
|
const name = RES_NAME[k] ?? k
|
|
parts.push(v > 0 ? `${name}+${v}` : `${name}${v}`)
|
|
}
|
|
}
|
|
if (e.rep) parts.push(`声望${e.rep > 0 ? '+' : ''}${e.rep}`)
|
|
if (e.relation) {
|
|
for (const v of Object.values(e.relation)) parts.push(v > 0 ? `交好+${v}` : `交恶${v}`)
|
|
}
|
|
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}`)
|
|
}
|
|
if (e.addTech) parts.push('得功法')
|
|
if (e.mission) parts.push('触发任务')
|
|
if (e.techniqueChance) parts.push(`功法机率+${Math.round(e.techniqueChance * 100)}%`)
|
|
if (e.artifactChance) parts.push(`宝器机率+${Math.round(e.artifactChance * 100)}%`)
|
|
if (e.raid) parts.push('来敌犯境')
|
|
if (e.trib) {
|
|
parts.push(e.trib.mode === 'rash' ? '硬渡天劫' : e.trib.mode === 'guard' ? '护法渡劫' : '渡劫延一年')
|
|
}
|
|
if (e.feisheng) parts.push(e.feisheng.stay ? '留世不飞升' : '飞升离世')
|
|
return parts.join(' · ')
|
|
}
|
|
|
|
export function EventModal() {
|
|
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
|
const pendingEventDef = useGameStore((s) => s.pendingEventDef)
|
|
const world = useGameStore((s) => s.world)
|
|
const bump = useGameStore((s) => s.bump)
|
|
const setSpeed = useGameStore((s) => s.setSpeed)
|
|
const [squadPicking, setSquadPicking] = useState(false)
|
|
const [squadSel, setSquadSel] = useState<string[]>([])
|
|
const [formation, setFormation] = useState<FormationId>('vanguard')
|
|
if (!pendingEventId || !pendingEventDef || !world) return null
|
|
|
|
const raidIdx = pendingEventDef.options.findIndex((o) => o.eff.raid || o.eff.tournament)
|
|
const squadPool: Character[] =
|
|
raidIdx >= 0
|
|
? world.aliveMembers()
|
|
.filter((c) => world.ageOf(c) >= 16 && c.state !== 'expedition')
|
|
.sort((a, b) => rankPower(world, b) - rankPower(world, a))
|
|
: []
|
|
|
|
const closeModal = () => {
|
|
useGameStore.setState({ pendingEventId: undefined, pendingEventDef: undefined })
|
|
setSquadPicking(false)
|
|
setSquadSel([])
|
|
bump()
|
|
}
|
|
|
|
const choose = (idx: number) => {
|
|
const isRaid = raidIdx >= 0 && raidIdx === idx
|
|
if (isRaid && !squadPicking) {
|
|
setSquadPicking(true)
|
|
setSquadSel(squadPool.slice(0, 4).map((c) => c.id))
|
|
return
|
|
}
|
|
applyEventChoice(world, pendingEventId, idx, isRaid ? squadSel : undefined, undefined, isRaid ? formation : undefined)
|
|
closeModal()
|
|
const sp = useGameStore.getState().speed
|
|
if (sp > 1) setSpeed(1)
|
|
}
|
|
|
|
const toggleSquad = (id: string) => {
|
|
setSquadSel((old) =>
|
|
old.includes(id) ? old.filter((x) => x !== id) : old.length >= 4 ? [...old.slice(1), id] : [...old, id]
|
|
)
|
|
}
|
|
|
|
const dorm = () => {
|
|
// 「稍后再议」:把事件按回队列,不消灭、不误弃
|
|
world.requeueEvent(pendingEventId)
|
|
closeModal()
|
|
}
|
|
|
|
return (
|
|
<ModalShell
|
|
title={pendingEventDef.name}
|
|
category={`${eventCategoryName(pendingEventDef.category)}${pendingEventDef.once ? ' · 仅此一次' : ''}`}
|
|
width={680}
|
|
allowOuter={false}
|
|
onClose={dorm}
|
|
closeLabel="稍后再议"
|
|
footer={
|
|
squadPicking ? (
|
|
<>
|
|
<button
|
|
className="btn"
|
|
onClick={() => {
|
|
setSquadPicking(false)
|
|
setSquadSel([])
|
|
}}
|
|
>
|
|
返回重选
|
|
</button>
|
|
<button className="btn btn-primary" disabled={squadSel.length === 0} onClick={() => choose(raidIdx)}>
|
|
{squadSel.length === 0 ? '族中无可战之人' : `出战!(共${squadSel.length}人)`}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<button className="btn" onClick={dorm}>稍后再议</button>
|
|
)
|
|
}
|
|
>
|
|
<div className="modal-text">{pendingEventDef.text}</div>
|
|
|
|
{squadPicking && (
|
|
<div className="modal-squad" style={{ marginBottom: 14 }}>
|
|
<div className="dim" style={{ marginBottom: 6 }}>迎战·列阵(默认四人,可改动)</div>
|
|
<div className="formation-pick" style={{ marginBottom: 8 }}>
|
|
{(Object.keys(FORMATIONS) as FormationId[]).map((fid) => (
|
|
<div
|
|
key={fid}
|
|
className={`tag ${formation === fid ? 'gold-t' : ''}`}
|
|
style={{ cursor: 'pointer', padding: '3px 10px' }}
|
|
onClick={() => setFormation(fid)}
|
|
title={FORMATIONS[fid].desc}
|
|
>
|
|
{FORMATIONS[fid].name}·{FORMATIONS[fid].desc}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="squad-picker">
|
|
{squadPool.map((c) => (
|
|
<div
|
|
key={c.id}
|
|
className={`squadian ${squadSel.includes(c.id) ? 'sel' : ''}`}
|
|
onClick={() => toggleSquad(c.id)}
|
|
>
|
|
{c.name}·{describeRealm(c.realm)}·战力{Math.round(combatPowerOf(world, c))}
|
|
</div>
|
|
))}
|
|
{squadPool.length === 0 && <span className="dim2">无可出战之人</span>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="modal-opts">
|
|
{squadPicking ? (
|
|
<div className="dim2">依选迎战(可继续点选调整阵容)</div>
|
|
) : (
|
|
pendingEventDef.options.map((o, i) => {
|
|
const isRaid = raidIdx === i
|
|
return (
|
|
<button key={i} className="opt-btn" onClick={() => choose(i)}>
|
|
{o.label}
|
|
{isRaid && <span className="hint">点将迎战 · 可自选阵容</span>}
|
|
{!isRaid && o.hint && <span className="hint">{o.hint}</span>}
|
|
<span className="opt-eff">{describeEff(o.eff as never)}</span>
|
|
</button>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
</ModalShell>
|
|
)
|
|
}
|