From 0f0b2770fe190a51dca8c5b0781731ca7e1d872e Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 11 Oct 2026 13:44:43 +0800 Subject: [PATCH] feat(09-06): add enemy handbook list UI (book.tsx) - Book / BookUI bound to props.state.enemyContext - dedupe enemies by base.code, vertical Scroll list - 9 columns: icon/name/specials/hp/atk/def/money/exp + damage/critical/reduce/1-def - critical via calculateEnemyCritical (base path); 1-def via with(isolated +1 def) - click row opens BookDetailUI; bottom-right exit closes instance --- packages-user/client-modules/src/ui/book.tsx | 235 +++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 packages-user/client-modules/src/ui/book.tsx diff --git a/packages-user/client-modules/src/ui/book.tsx b/packages-user/client-modules/src/ui/book.tsx new file mode 100644 index 0000000..c3dc2fb --- /dev/null +++ b/packages-user/client-modules/src/ui/book.tsx @@ -0,0 +1,235 @@ +import { GameUI, SetupComponentOptions } from '@motajs/system'; +import { computed, defineComponent } from 'vue'; +import { Font } from '@motajs/render'; +import { ITileLocator } from '@motajs/common'; +import { Scroll } from '@user/client-base'; +import { IEnemyView } from '@user/data-system'; +import { IReadonlyEnemy } from '@user/data-base'; +import { IEnemyAttr } from '@user/data-common'; +import { BookDetailUI } from './bookDetail'; +import { FULL_LOC, MAIN_HEIGHT, MAIN_WIDTH } from '../shared'; +import { IUIPropsBase } from './types'; + +export interface BookProps extends IUIPropsBase {} + +const bookProps = { + props: ['controller', 'instance', 'state'] +} satisfies SetupComponentOptions; + +/** 列表中每一行的高度 */ +const ROW_HEIGHT = 72; +/** 列表区域的起始纵坐标 */ +const LIST_TOP = 44; +/** 列表区域的横坐标 */ +const LIST_LEFT = 16; +/** 列表区域的宽度 */ +const LIST_WIDTH = MAIN_WIDTH - 32; +/** 列表区域的高度,底部留出退出按钮的空间 */ +const LIST_HEIGHT = MAIN_HEIGHT - LIST_TOP - 36; +/** 数值列区域的起始横坐标 */ +const STAT_LEFT = 240; + +/** 怪物名称字体 */ +const nameFont = Font.defaults({ size: 15, weight: 700 }); +/** 特殊名与属性字体 */ +const statFont = Font.defaults({ size: 12 }); + +export const Book = defineComponent(props => { + const ctx = props.state.enemyContext; + + // 按怪物图块数字去重,每个数字保留一个代表视图 + const enemies = computed(() => { + const map = new Map>(); + for (const pair of ctx.iterateEnemy()) { + const base = pair[1].getBaseEnemy(); + if (!map.has(base.code)) map.set(base.code, pair[1]); + } + return Array.from(map.values()); + }); + + const format = (value: number) => { + return core.formatBigNumber(value); + }; + + /** + * 以基础属性路径计算怪物临界信息,已达临界上界或未绑定伤害系统时返回 null + */ + const getCritical = ( + base: IReadonlyEnemy, + locator: ITileLocator | null + ) => { + const damage = ctx.getDamageSystem(); + // TODO: 伤害系统未绑定或已达临界上界时,伤害 / 临界 / 减伤 / 「1 防」留空 + if (!damage) return null; + const first = damage + .calculateEnemyCritical(base, 'atk', locator) + .next(); + if (first.done) return null; + return first.value; + }; + + /** + * 以基础属性路径计算「1 防」时的伤害信息,即隔离属性增加 1 点防御后的战斗信息 + */ + const getDef1 = ( + base: IReadonlyEnemy, + locator: ITileLocator | null + ) => { + const damage = ctx.getDamageSystem(); + if (!damage) return null; + const isolated = props.state.hero.getIsolatedAttribute(); + isolated.add('def', 1); + const first = damage + .with(isolated) + .calculateEnemyCritical(base, 'atk', locator) + .next(); + if (first.done) return null; + return first.value; + }; + + const specialText = (base: IReadonlyEnemy) => { + const names: string[] = []; + for (const special of base.iterateSpecials()) { + names.push(special.getSpecialName()); + } + return names.join(' '); + }; + + const statX = (index: number) => { + return STAT_LEFT + (index * (MAIN_WIDTH - STAT_LEFT)) / 5; + }; + + const openDetail = (view: IEnemyView) => { + props.controller.open(BookDetailUI, { enemy: view }); + }; + + const exit = () => props.controller.close(props.instance); + + return () => ( + + + + + {enemies.value.map((view, i) => { + const base = view.getBaseEnemy(); + const locator = ctx.getEnemyLocatorByView(view); + const critical = getCritical(base, locator); + const def1 = getDef1(base, locator); + const y = i * ROW_HEIGHT; + return ( + openDetail(view)} + > + + + + + + + + + {critical !== null && ( + + )} + {critical !== null && ( + + )} + {critical !== null && ( + + )} + {critical !== null && def1 !== null && ( + + )} + + ); + })} + + + + ); +}, bookProps); + +export const BookUI = new GameUI('book', Book);