diff --git a/README-zh_CN.md b/README-zh_CN.md index e44f7f5..81aa6df 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -1,56 +1,19 @@ -# Life Restart +# 人生重开模拟器 [English](./README.md) | 简体中文 -## 简介 +## 开发环境 -- Game Life Restart +包管理: `pnpm` +运行时: `bun` ## 使用 -
-网页版 -
+### 开发 ```bash -# 1. 下载项目代码 -git clone https://github.com/VickScarlet/lifeRestart.git my-project - -# 2. 进入目录安装依赖 -cd my-project -pnpm install - -# 3. 转换XLSX表 -pnpm xlsx2json - -# 4. 启动本地开发服务器 +pnpm build:data pnpm dev -``` - -启动完成后打开浏览器访问 [http://localhost:5173](http://localhost:5173)。 - -
- -
-控制台版本 -
- -```bash -node repl -``` - -
- -## 其他版本 - -
-版本列表 -
- -- Cocos版:[gameall3d/LifeRestart_Cocos](https://github.com/gameall3d/LifeRestart_Cocos) - -
- -> 更多信息请参考 [官网文档](https://liferestart.syaro.io/)。 +``` \ No newline at end of file diff --git a/README.md b/README.md index 8494368..a34610d 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,19 @@ -# Life Restart +# Remake English | [简体中文](./README-zh_CN.md) -## Introduction +## Environment -- Game Life Restart +Package Manager: `pnpm` +Runtime: `bun` ## Usage -
-Web Version -
- +### Dev ```bash -# 1. Clone project code. -git clone git@github.com:VickScarlet/lifeRestart.git my-project -cd my-project - -# 2. Installation dependence. -pnpm install - -# 3. Transform XLSX -pnpm xlsx2json - -# 4. Start local dev server. +pnpm build:data pnpm dev -``` - -After the startup is complete, open a browser and visit [http://localhost:5173](http://localhost:5173). -
- -
-Command Line Version -
- -```bash -node repl -``` - -
- -## Other Version - -
-Versions -
- -- Cocos Ver: [gameall3d/LifeRestart_Cocos](https://github.com/gameall3d/LifeRestart_Cocos) - -
- -> More instructions at [documentation](https://liferestart.syaro.io/). +``` \ No newline at end of file diff --git a/apps/web/src/containers/Play.tsx b/apps/web/src/containers/Play.tsx index cf33a7f..0b77bf4 100644 --- a/apps/web/src/containers/Play.tsx +++ b/apps/web/src/containers/Play.tsx @@ -6,6 +6,7 @@ import type { Log } from '@remake/hooks' import { properties } from '@/display' import { achievements, events, talents } from '@remake/data' import { AutoInterval } from '@/config' +import { format } from '@remake/vitex' import './Play.css' function LogTalent({ id }: { id: number }) { @@ -25,12 +26,19 @@ function LogTalents({ items }: { items: number[] }) { return } +const year = new Date().getFullYear() interface LogEventProps { id: number post: boolean + index: number } -function LogEvent({ id, post }: LogEventProps) { - const { event, postEvent, grade } = events.get(id)! +function LogEvent({ id, post, index }: LogEventProps) { + let { event, postEvent, grade, format: f } = events.get(id)! + if (f) { + const g = (key: string) => ({ CurrentYear: year + index })[key] + event = format(event, g) + if (post && postEvent) postEvent = format(postEvent, g) + } return ( <>
  • {event}
  • @@ -41,10 +49,10 @@ function LogEvent({ id, post }: LogEventProps) { ) } -function LogEvents({ items }: { items: number[] }) { +function LogEvents({ items, index }: { items: number[]; index: number }) { const last = items.length - 1 const els = items.map((id, i) => ( - + )) return } @@ -66,13 +74,13 @@ function LogAchievements({ items }: { items: number[] }) { return } -function Log({ log }: { log: Log }) { +function Log({ log, index }: { log: Log; index: number }) { return (
  • {log.age}岁
    - +
  • @@ -88,25 +96,40 @@ interface PropProps { function Prop({ prop, value, grade }: PropProps) { const prevRef = useRef(value) const [trend, setTrend] = useState<'up' | 'down' | 'normal'>('normal') - const [animCount, setAnimCount] = useState(0) + const [flip, setFlip] = useState(0) + const [displayValue, setDisplayValue] = useState(value) useEffect(() => { const prev = prevRef.current if (value === prev) return prevRef.current = value setTrend(value > prev ? 'up' : 'down') - setAnimCount(c => c + 1) - const timer = setTimeout(() => { - setTrend('normal') - }, 2000) - return () => clearTimeout(timer) + setFlip(f => (f + 1) % 2) + let startTimestamp: number | null = null + const duration = 400 + const startValue = displayValue + let end = false + const step = (timestamp: number) => { + if (!startTimestamp) startTimestamp = timestamp + const progress = timestamp - startTimestamp + const progressRatio = Math.min(progress / duration, 1) + const easeOutQuad = progressRatio * (2 - progressRatio) + const currentDec = startValue + (value - startValue) * easeOutQuad + setDisplayValue(Math.round(currentDec)) + if (!end && progress < duration) requestAnimationFrame(step) + } + requestAnimationFrame(step) + const timer = setTimeout(() => setTrend('normal'), 2000) + return () => { + clearTimeout(timer) + setDisplayValue(value) + end = true + } }, [value]) return ( -
  • +
  • {properties[prop]} - {value} + {displayValue}
  • ) } @@ -160,7 +183,7 @@ export function Play() { ref={logRef} > {logs.map((log, index) => ( - + ))}
    diff --git a/apps/web/src/containers/Summary.tsx b/apps/web/src/containers/Summary.tsx index 9255579..f9c5bb3 100644 --- a/apps/web/src/containers/Summary.tsx +++ b/apps/web/src/containers/Summary.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useRef } from 'react' +import { useEffect, useState, useRef, useMemo } from 'react' import { usePicked, useEnd, useProfile } from '@remake/hooks' import { useEndJudge } from '@/hooks/judge' import { properties, judgeDisplay } from '@/display' @@ -27,26 +27,24 @@ interface TalentListProps { } function TalentList({ picked, picker }: TalentListProps) { const [profile] = useProfile() - const [talents, setTalents] = useState(usePicked()) + const items = usePicked() + const talents = useMemo(() => { + if (!isDev || !profile.locked) return items + return new Set([...items, ...profile.locked]) + }, []) const hasInitialized = useRef(false) - useEffect(() => { if (!profile.locked) return - let currentTalents = talents - if (isDev) { - currentTalents = new Set([...talents, ...profile.locked]) - setTalents(currentTalents) - } - if (!hasInitialized.current) { for (const id of profile.locked) { - if (currentTalents.has(id) && !picked.has(id)) { + if (talents.has(id) && !picked.has(id)) { picker(id) } } hasInitialized.current = true } - }, [profile, talents, picked, picker]) + }, [picked, picker]) + return (
      {Array.from(talents, id => ( diff --git a/docs/remake.md b/docs/remake.md deleted file mode 100644 index 4d9b2a5..0000000 --- a/docs/remake.md +++ /dev/null @@ -1,17 +0,0 @@ -## 流程 - -重开 - 初始化 - 抽天赋(包含继承的天赋) - 天赋选择(检测天赋冲突) - 天赋替换(检测天赋冲突) - 点数分配(天赋额外点数) - 游戏循环 - 年龄增加一岁 - 天赋触发 - 事件触发(从对应年龄事件池随机事件) - 判断是否结束(生命值小于 1 结束) - 总评 - 天赋继承 - 存档数据 - 重回首页 \ No newline at end of file diff --git a/packages/condition/index.ts b/packages/condition/index.ts index 629d0f2..032e033 100644 --- a/packages/condition/index.ts +++ b/packages/condition/index.ts @@ -188,16 +188,3 @@ function checkProp(property: PropertyContainer, condition: string): boolean { return false } } - -export function extractMaxTriggers(condition: string): number { - // Assuming only age related talents can be triggered multiple times. - const RE_AGE_CONDITION = /AGE\?\[([0-9,]+)\]/ - const matchObject = RE_AGE_CONDITION.exec(condition) - - if (matchObject === null) { - // Not age related, single trigger. - return 1 - } - - return matchObject[1]?.split(',')?.length ?? 1 -} diff --git a/packages/core/src/achievement.spec.ts b/packages/core/src/achievement.spec.ts index 3177c78..69aae26 100644 --- a/packages/core/src/achievement.spec.ts +++ b/packages/core/src/achievement.spec.ts @@ -17,6 +17,7 @@ describe('Achievement', () => { intelligence: 5, strength: 5, money: 5, + spirit: 5, } test('trigger [times:500]', () => { diff --git a/packages/core/src/event.spec.ts b/packages/core/src/event.spec.ts index 7ccf126..198ed6b 100644 --- a/packages/core/src/event.spec.ts +++ b/packages/core/src/event.spec.ts @@ -16,6 +16,7 @@ describe('Event', () => { intelligence: 10, strength: 0, money: 0, + spirit: 5, } test('branch 10003:0 [玉佩]', () => { diff --git a/packages/core/src/game.spec.ts b/packages/core/src/game.spec.ts index e23b4ba..cddf7f7 100644 --- a/packages/core/src/game.spec.ts +++ b/packages/core/src/game.spec.ts @@ -17,6 +17,7 @@ describe('Achievement', () => { intelligence: 5, strength: 5, money: 5, + spirit: 5, } test('talentsPicked', () => { diff --git a/packages/core/src/state.spec.ts b/packages/core/src/state.spec.ts index 57c3152..5d067da 100644 --- a/packages/core/src/state.spec.ts +++ b/packages/core/src/state.spec.ts @@ -2,7 +2,7 @@ import { expect, test, describe } from 'bun:test' import { parse, type ConditionNode } from '@remake/condition' import { achievements, events, talents } from '@remake/data' -import type { GameState, ProfileState, Properties } from './state' +import type { FlatState, GameState, ProfileState, Properties } from './state' import { createFlatState, createHLProperties, propsEffect } from './state' import { SupportedFlatStateKeys } from './state' import { enableMapSet } from 'immer' @@ -11,7 +11,10 @@ enableMapSet() /** * 核心递归辅助函数:从多维语法树节点中提取所有属性名 */ -function extractKeysFromNode(node: ConditionNode, properties: Set) { +function extractKeysFromNode( + node: ConditionNode, + properties: Set, +) { if (Array.isArray(node)) { for (const subNode of node) { extractKeysFromNode(subNode, properties) @@ -22,7 +25,7 @@ function extractKeysFromNode(node: ConditionNode, properties: Set) { // 匹配操作符(>) { /** * 从条件字符串中提取所有使用到的属性键名 */ -function getPropertiesFromCondition(condition: string): Set { +function getPropertiesFromCondition(condition: string): Set { const parsedConditions = parse(condition) - const properties = new Set() + const properties = new Set() extractKeysFromNode(parsedConditions, properties) return properties } @@ -205,7 +208,13 @@ describe('State', () => { ) }) - const allocation = { charm: 5, intelligence: 5, strength: 5, money: 5 } + const allocation = { + charm: 5, + intelligence: 5, + strength: 5, + money: 5, + spirit: 5, + } test('createHLProperties', () => { const props = createHLProperties(allocation) expect(props.current.charm).toBe(allocation.charm) @@ -213,7 +222,7 @@ describe('State', () => { expect(props.current.strength).toBe(allocation.strength) expect(props.current.money).toBe(allocation.money) expect(props.current.age).toBe(-1) - expect(props.current.spirit).toBe(0) + expect(props.current.spirit).toBe(5) }) test('propsEffect', () => { diff --git a/packages/core/src/talent.spec.ts b/packages/core/src/talent.spec.ts index 11a8086..9d5a2a6 100644 --- a/packages/core/src/talent.spec.ts +++ b/packages/core/src/talent.spec.ts @@ -30,11 +30,11 @@ describe('Talent', () => { test('pull external', () => { const p = produce(profile, draft => { - draft.lockedTalent = 1001 + draft.locked = [1001] }) const result = pull(options, p) expect(result.length).toBe(options.count) - expect(result).toContain(p.lockedTalent!) + expect(result).toContain(p.locked![0]!) }) test('pull rate', () => { diff --git a/packages/data/BUGS.md b/packages/data/BUGS.md new file mode 100644 index 0000000..132dbab --- /dev/null +++ b/packages/data/BUGS.md @@ -0,0 +1,9 @@ +## Bugs + +### 复活Bug + age 400 + -> 寿元终 + -> 天赋复活 + age 401 + -> 无可随机事件 + -> 报错 \ No newline at end of file diff --git a/packages/data/src/event.types.ts b/packages/data/src/event.types.ts index 82380ce..08f5a0b 100644 --- a/packages/data/src/event.types.ts +++ b/packages/data/src/event.types.ts @@ -47,6 +47,8 @@ export type Event = { readonly exclude?: string /** 分支路线 */ readonly branch?: EventBranch[] + /** 是否需要格式化 */ + readonly format?: boolean } // @vt-types-end diff --git a/packages/data/src/event.xlsx b/packages/data/src/event.xlsx index 7dcce29..b418804 100644 Binary files a/packages/data/src/event.xlsx and b/packages/data/src/event.xlsx differ diff --git a/packages/vitex/index.ts b/packages/vitex/index.ts index b846f14..4e5bade 100644 --- a/packages/vitex/index.ts +++ b/packages/vitex/index.ts @@ -43,3 +43,18 @@ export function keys( const filterSet = new Set(filter) return allKeys.filter(key => !filterSet.has(key)) as Exclude[] } +// 定义获取字段值的方法签名 +export type GetValueFn = ( + fieldName: string, +) => string | number | boolean | null | undefined +export function format(str: string, getValueFn: GetValueFn): string { + if (!str) return '' + return str.replace( + /\{([^}]+)\}/g, + (match: string, fieldName: string): string => { + const value = getValueFn(fieldName) + if (value === undefined || value === null) return match + return String(value) + }, + ) +}