feat: add age-based event gating, event timer, and UI polish

- Add minAge/maxAge to events so infants can't go treasure hunting
- Cache event panel DOM to prevent high-speed button destruction
- Add 10s auto-select countdown for choice events
- Fix event title/text field mapping (name/description → title/text)
- Add rotating clock icon for time flow feedback
- Fix speed/pause button active states
- Fix shop affordability check (disable + show insufficient money)
- Add red styling for unmet choice requirements
- Fix log re-rendering on every tick
This commit is contained in:
2026-05-13 09:09:42 +00:00
parent a05225b514
commit 3f741b4f0a
41 changed files with 9847 additions and 651 deletions
+663
View File
@@ -0,0 +1,663 @@
# 轮回录 - 游戏设计文档
## 一、游戏概述
**类型** Rogue-lite + Incremental RPG
**核心体验**: 在无限轮回中积累经验与记忆,逐步解锁世界真相,最终对抗隐藏的终极威胁。
**目标玩家**: 喜欢长期成长、策略试错、叙事探索的玩家。
**单局时长**: 一局人生 5~15 分钟(加速模式下),完整通关需 50~100+ 世。
---
## 二、核心循环(Core Loop
```
出生(随机出身 + 词条池抽取)
日常推进(文本事件自动流过 / 选择事件 10秒倒计时)
积累经验 → 职业晋升 → 解锁能力
死亡(自然 / 外敌 / 事件风险)
结算(元经验 + 新词条解锁 + 历史对比)
重生(保留元经验 / 记忆 / 词条池,外敌进度部分继承)
(循环)
```
**关键张力**:每世都是不可预测的(随机出身、词条、事件),但跨世成长是确定可感的(元经验压缩前期、词条池扩大、记忆触发新选项)。
---
## 三、系统详解
### 3.1 时间系统
| 项目 | 设计 |
|---|---|
| 最小单位 | 天 |
| 显示单位 | 天 / 年 / 年龄 |
| 加速档位 | 1x1天/秒)、10x、100x、MAX(瞬间过完一年) |
| 加速时显示 | 文本事件快速滚动(日志流),选择事件暂停等玩家 |
| 存档 | 每年自动存档 + 手动存档 |
**一天内的流程**
1. 推进日期
2. 检查固定事件触发
3. 从事件池随机抽取文本事件(概率控制)
4. 检查是否有选择事件触发
5. 推进经验、属性自然增长
6. 推进外敌进度
7. 检查死亡条件
### 3.2 事件系统
#### 事件类型
| 类型 | 触发方式 | 玩家交互 | 占比 |
|---|---|---|---|
| **文本事件** | 概率触发 / NPC事件线 | 无,自动流过 | 80% |
| **选择事件** | 固定时间点 / 条件触发 | 10秒倒计时,不选默认 | 15% |
| **重大事件** | 事件链终点 / 外敌阈值 | 必须选择 | 5% |
#### 事件结构
```javascript
{
id: 'event_001',
title: '山中奇遇',
// 触发条件
condition: (state) => state.age >= 5 && state.age <= 10
&& !state.worldFlags.met_old_taoist,
// 概率权重(同条件事件竞争)
weight: 10,
// 是否为选择事件
isChoice: true,
// 默认选项索引(倒计时结束时自动选)
defaultChoice: 0,
// 选项
choices: [
{
text: '跟随老者',
// 硬门槛判定
requirement: (state) => state.stats.wisdom >= 3,
// 效果
effect: (state) => {
state.worldFlags.met_old_taoist = true;
return { unlockCareer: 'cultivation', exp: { cultivation: 100 } };
}
},
{
text: '转身离开',
effect: (state) => ({ exp: { hunter: 20 } })
}
],
// 事件链标记
flags: { saw_mountain_path: true },
// 后置事件(本事件触发后,后置事件进入候选池)
unlocks: ['event_002', 'event_003']
}
```
#### 标记系统(防逻辑错误)
```javascript
worldFlags: {
'npc.zhangsan.alive': true, // NPC生死
'npc.zhangsan.married': false, // NPC婚姻状态
'event.mountain_discovery.happened': true, // 事件已发生
'enemy.north.barbarian_invasion': false, // 外敌入侵状态
}
```
任何事件在修改标记前必须检查依赖标记。例如:"张三死了"的事件触发后,`npc.zhangsan.alive` 设为 false,后续所有涉及张三的事件必须检查此标记。
### 3.3 职业系统
#### 职业结构
职业呈**线性晋升链**,无"转换"概念。初始只有少数基础职业,等级达标后解锁后续职业。
**示例职业链**
| 层级 | 职业示例 | 类型 | 晋升条件 |
|---|---|---|---|
| 基础 | 混混、磨刀学徒、农夫、书童 | 短线 | 无 |
| 进阶 | 红棍、打刀下手、猎户、秀才 | 短线 | 基础职业 Lv.5 |
| 中阶 | 香主、锻刀师傅、商人、举人 | 中线 | 进阶职业 Lv.10 |
| 高阶 | 堂主、炼刀匠人、大儒、进士 | 中线 | 中阶职业 Lv.20 |
| 顶级 | 舵主、铸剑师、一代宗师、宰相 | 长线 | 高阶职业 Lv.30 |
| 终极 | 帮主、仙匠、圣贤、帝王 | 极长线 | 顶级职业 Lv.50 |
**修仙者**不是独立职业,而是多条路线的终极交汇点。例如:
- 锻刀师傅 → 炼刀匠人 → 铸剑师 → (悟剑道)→ 剑修
- 大儒 → (悟道)→ 儒仙
- 红棍 → (武道通神)→ 武仙
#### 职业属性
```javascript
{
id: 'hoodlum',
name: '混混',
type: 'short', // short / medium / long / ultimate
maxLevel: 10,
baseExp: 20,
expCurve: 1.15, // 每级经验需求增长
// 自然成长(每天自动获得的经验)
dailyExp: 5,
// 晋升链
promotesTo: 'red_stick', // Lv.10 解锁
promoteRequirement: { level: 10 },
// 能力解锁(随等级解锁)
abilities: [
{ level: 3, id: 'street_fight', name: '街头斗殴', effect: '战斗事件成功率+10%' },
{ level: 7, id: 'intimidate', name: '威吓', effect: 'NPC事件中威慑选项可用' },
]
}
```
#### 多职业并行
玩家可以同时拥有多个职业,每个职业独立计算等级和经验。例如:白天当农夫(farmer Lv.5),晚上混帮派(hoodlum Lv.3)。
### 3.4 成长系统
#### 三层成长
| 层级 | 名称 | 跨轮保留 | 作用 | 示例 |
|---|---|---|---|---|
| **L1** | 元经验 | 是 | 压缩前期成长曲线 | 剑术元经验 500 → 前 50 级 +80% 速度 |
| **L2** | 能力 | 是 | 解锁新技能/选项 | 剑术 Lv.30 留下"剑气感悟",下世 Lv.10 即可用剑气 |
| **L3** | 词条 | 是 | 重生时的随机特性池 | 解锁"孤儿"词条,重生时可能随机到 |
#### 元经验公式
```javascript
// 元经验加成(对数衰减,永不归零)
function metaBoost(careerId, currentLevel, metaExp) {
return Math.log(1 + metaExp / 100) * Math.exp(-currentLevel / 80);
}
// 总经验倍率
function totalExpMultiplier(state, careerId) {
const career = state.careers[careerId];
const meta = state.metaExp[careerId] || 0;
const level = career?.level || 0;
let multiplier = 1;
// 元经验加成
multiplier += metaBoost(careerId, level, meta);
// 天赋加成
for (const t of state.talents) {
if (t.effect.target === careerId || t.effect.target === 'all') {
multiplier += t.effect.value;
}
}
// 记忆加成
for (const m of state.memories) {
if (m.effect?.target === careerId) {
multiplier += m.effect.value;
}
}
return multiplier;
}
```
#### 能力链
不是"境界"概念,而是**具体技能的解锁**。例如:
**剑术能力链**
- Lv.1~9:基础剑法(数值成长)
- Lv.10:解锁【剑气】(攻击范围扩大)
- Lv.15:解锁【御剑】(移动速度提升)
- Lv.25:解锁【万剑归宗】(群体攻击)
- Lv.40:解锁【人剑合一】(攻击无视防御)
**跨世传承**:某一世把剑术练到 Lv.30,死亡后留下"剑气感悟"记忆。下一世再练剑术时,Lv.10 自动解锁【剑气】(不需要重新练到 Lv.25)。
### 3.5 词条系统
#### 词条定义
词条是重生时随机抽取的"身份/天赋/携带物",影响整局游戏。
```javascript
{
id: 'orphan',
name: '孤儿',
type: 'identity', // identity / talent / item / curse
desc: '无父无母,街头生存经验丰富',
effect: { target: 'hoodlum', expRate: 0.3, body: 1 },
// 解锁条件
unlockCondition: (history) => history.some(h => h.family < -1)
}
```
#### 词条类型
| 类型 | 示例 | 效果 |
|---|---|---|
| **身份** | 孤儿、皇室后裔、商贾之子 | 影响出身、初始事件池 |
| **天赋** | 剑骨、灵根、过目不忘 | 直接影响某职业成长 |
| **携带物** | 祖传玉佩、神秘种子、旧日记 | 触发特定事件链 |
| **诅咒** | 短命、招灾、孤星 | 负面效果,但可能解锁隐藏路线 |
#### 解锁机制
- **等级解锁**:剑术 Lv.30 → 解锁"剑气"词条
- **事件解锁**:完成"梦中悟道"事件链 → 解锁"悟道"词条
- **条件解锁**:活到 100 岁 → 解锁"长寿"词条;被毒杀 → 解锁"毒物警觉"词条
- **成就解锁**:累计 10 世当混混 → 解锁"江湖中人"词条
#### 重生抽取
每世重生时,从已解锁词条池中随机抽取 3~5 个。词条池越大,组合越丰富。
### 3.6 NPC系统
#### NPC结构
```javascript
{
id: 'zhangsan',
name: '张三',
// NPC有自己的事件线
eventLine: [
{ day: 100, event: 'zhangsan_marriage', condition: (state) => state.worldFlags.npc.zhangsan.alive },
{ day: 500, event: 'zhangsan_death', condition: (state) => state.worldFlags.npc.zhangsan.married === false },
],
// 与玩家的关系
relation: {
trust: 0, // -100 ~ 100
favor: 0, // -100 ~ 100
},
// NPC可以给玩家东西
gifts: [
{ condition: (state) => state.npcs.zhangsan.favor > 50, item: 'old_sword' }
]
}
```
#### NPC事件对玩家的影响
| 影响类型 | 示例 |
|---|---|
| **纯文本** | "张三今天娶了李四"(仅信息) |
| **关系影响** | "张三向你求助"(接受+信任,拒绝-信任) |
| **赠送物品** | "张三送你一把旧剑"(激活词条或加成) |
| **世界事件前置** | "张三发现了秘境入口"(开启新事件链) |
| **连锁反应** | "张三死了,他的仇家找上了你" |
### 3.7 外敌系统
#### 三相威胁
| 相 | 名称 | 增长条件 | 对抗方式 |
|---|---|---|---|
| **军事** | 北方蛮族 | 王朝不稳时加速 | 武道/修仙等级 |
| **精神** | 域外天魔 | 修仙者死亡时泄漏 | 悟性/意志力 |
| **政治** | 王朝崩坏 | 长期无人治国 | 权势/民心 |
#### 三相联动
```
王朝崩坏 +10% → 蛮族入侵加速 +20%
蛮族入侵 +10% → 民心下降 → 王朝崩坏加速 +15%
天魔侵蚀 +10% → 修仙者恐慌 → 更多修仙者死亡 → 天魔加速
```
#### 隐藏的第四相
当三相都稳定(均 < 20%)时,触发"真相浮现"事件链:
- 三相只是表象,真正的威胁是"轮回本身"
- 玩家的无限轮回正在撕裂世界
- 最终目标:找到终止轮回或拯救世界的方法
#### 跨轮影响
- 某一世成功击退蛮族 → 下一世蛮族起点降低
- 某一世天魔侵蚀严重 → 下一世天魔起点更高
- 三相整体进度跨轮部分继承(如保留 30%)
### 3.8 存档系统
| 类型 | 触发方式 | 保存内容 |
|---|---|---|
| **自动存档** | 每年年末 | 完整状态 |
| **手动存档** | 玩家主动 | 完整状态 |
| **死亡存档** | 死亡瞬间 | 本世完整记录(用于历史对比) |
#### 存档结构
```javascript
{
version: '0.1',
slot: 1,
timestamp: Date.now(),
state: { /* 完整游戏状态 */ },
history: [ /* 往世记录 */ ]
}
```
### 3.9 死亡结算
#### 结算内容
1. **本世回顾**
- 活了多久(天/年)
- 死因
- 职业成就(各职业最高等级)
- 走过的事件链
- 解锁的词条
- 对世界的影响(外敌变化)
2. **历史对比**
- 这一世 vs 上一世:多活了几年?
- 这一世 vs 历史最佳:职业等级差距?
- 累计元经验增长曲线
3. **元经验结算**
- 各职业经验 → 元经验(转化率 10%)
4. **新词条解锁提示**
- "解锁新词条:短命(被毒杀3次)"
---
## 四、数值设计
### 4.1 经验曲线
```javascript
// 升级所需经验
function expToNextLevel(careerConfig, currentLevel) {
return Math.floor(careerConfig.baseExp * Math.pow(careerConfig.expCurve, currentLevel));
}
// 示例:混混(baseExp=20, curve=1.15
// Lv.1 → 2: 23
// Lv.5 → 6: 40
// Lv.9 → 10: 71
// 示例:修仙者(baseExp=100, curve=1.25
// Lv.1 → 2: 125
// Lv.10 → 11: 931
// Lv.50 → 51: 约 60万
// Lv.99 → 100: 约 3万亿
```
### 4.2 元经验成长预期
| 轮回 | 单世主要成就 | 累计元经验(剑术) | 前30级加成 |
|---|---|---|---|
| 1 | 剑术 Lv.15 | 150 | +50% |
| 2 | 剑术 Lv.25 | 500 | +90% |
| 3 | 剑术 Lv.35 | 1200 | +120% |
| 5 | 剑术 Lv.50 | 4000 | +160% |
| 10 | 剑术 Lv.80 | 20000 | +200% |
| 20 | 剑术 Lv.100 | 80000 | +240% |
注:加成是"速度倍率",不是"直接加等级"。元经验让前期飞快,但瓶颈期(高等级)仍然需要多轮积累。
### 4.3 死亡概率
| 年龄 | 自然死亡概率/年 | 备注 |
|---|---|---|
| 0~50 | 0% | |
| 51~60 | 2% | |
| 61~70 | 8% | |
| 71~80 | 20% | |
| 81~90 | 45% | |
| 91~100 | 80% | |
| 100+ | 100% | 必然死亡 |
外敌死亡:三相任一达到 100% 时,每年 40% 概率直接死亡。
### 4.4 事件密度
| 年龄段 | 文本事件/年 | 选择事件 | 重大事件 |
|---|---|---|---|
| 幼年(0~6) | 20~30 | 2~3 | 0 |
| 少年(7~15) | 10~15 | 3~5 | 1 |
| 青年(16~25) | 5~8 | 2~3 | 1 |
| 壮年(26~40) | 3~5 | 2~3 | 1 |
| 中年(41~60) | 2~4 | 1~2 | 1 |
| 老年(61+) | 5~10 | 2~3 | 1 |
---
## 五、架构设计
### 5.1 模块结构
```
src/
engine/
state.js # 游戏状态管理
time.js # 时间推进、加速控制
eventEngine.js # 事件调度器(条件匹配 → 加权随机 → 执行)
expSystem.js # 经验计算、升级、能力解锁
death.js # 死亡判定、轮回结算
saveSystem.js # 存档/读档
flagSystem.js # 世界标记管理
content/
events/
index.js # 事件注册中心
child.js # 幼年事件
youth.js # 少年事件
adult.js # 成年事件
common.js # 通用日常事件
chains/ # 事件链定义
careers/
index.js # 职业注册中心
short/ # 短线职业
medium/ # 中线职业
long/ # 长线职业
ultimate/ # 终极职业
talents/
index.js # 词条注册中心
npcs/
index.js # NPC注册中心
ui/
renderer.js # 渲染器
eventPanel.js # 事件面板
logStream.js # 日志流
careerPanel.js # 职业面板
deathScreen.js # 死亡结算界面
main.js # 入口
```
### 5.2 数据流
```
用户交互(点击/加速/选择)
UI Layer → 调用 Engine API
Time.tick() / EventEngine.trigger() / ExpSystem.gain()
修改 State
State 变更通知 UI
UI 重新渲染
```
### 5.3 事件引擎核心逻辑
```javascript
class EventEngine {
constructor() {
this.eventPool = []; // 所有注册的事件
this.activeChains = []; // 当前活跃的事件链
}
// 注册事件
register(eventDef) {
this.eventPool.push(eventDef);
}
// 每tick调用
tick(state) {
// 1. 筛选满足条件的事件
const candidates = this.eventPool.filter(e =>
e.condition(state) &&
!state.triggeredEvents.has(e.id) &&
this.checkPrerequisites(e, state)
);
// 2. 按权重随机选择
const event = this.weightedRandom(candidates);
if (!event) return null;
// 3. 标记已触发
state.triggeredEvents.add(event.id);
// 4. 解锁后置事件
if (event.unlocks) {
event.unlocks.forEach(id => this.activateChain(id));
}
return event;
}
checkPrerequisites(event, state) {
if (!event.prerequisites) return true;
return event.prerequisites.every(flag => {
const negate = flag.startsWith('!');
const key = negate ? flag.slice(1) : flag;
const value = this.getFlag(state, key);
return negate ? !value : !!value;
});
}
}
```
---
## 六、UI设计
### 6.1 主界面布局
```
┌─────────────────────────────────────────────────────┐
│ [世数] 第42世 │ [时间] 第15年 第128天 │ [年龄] 15岁 │
├─────────────────────────────────────────────────────┤
│ 加速: [1x] [10x] [100x] [MAX] │ [暂停] [存档] │
├─────────────────────────────────────────────────────┤
│ │
│ 【事件面板】 │
│ (选择事件时显示倒计时) │
│ │
├─────────────────────────────────────────────────────┤
│ 【日志流】(文本事件滚动显示) │
│ [128天] 张三今天娶了李四 │
│ [129天] 你在街头遇到了一个老乞丐... │
│ [130天] 【选择事件】山中奇遇(倒计时: 7秒) │
│ │
├─────────────────────────────────────────────────────┤
│ 词条: [孤儿] [剑骨] [神秘种子] │
├─────────────────────────────────────────────────────┤
│ 属性: 体质12 悟性8 魅力5 气运7 │
├─────────────────────────────────────────────────────┤
│ 职业: │
│ 混混 Lv.7 [=====> ] 红棍(解锁条件: Lv.10) │
│ 磨刀学徒 Lv.3 [==> ] │
├─────────────────────────────────────────────────────┤
│ 外敌: 蛮族35% │ 天魔22% │ 崩坏18% │
├─────────────────────────────────────────────────────┤
│ 记忆: [剑气感悟] [毒物警觉] │
│ 元经验: 剑术+3200 混混+800 │
└─────────────────────────────────────────────────────┘
```
### 6.2 死亡结算界面
```
┌─────────────────────────────────────────────────────┐
│ 【身死道消】 │
│ │
│ 你活了 67 年 128 天 │
│ 死因:寿终正寝 │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 本世成就: │
│ • 混混 Lv.10 → 红棍(晋升!) │
│ • 剑术 Lv.35 │
│ • 走过事件链:山中奇遇 → 老者传道 → 宗门测试 │
│ • 解锁词条:剑气、江湖中人 │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 历史对比: │
│ 本世 vs 上世:多活了 12 年 ✓ │
│ 本世 vs 最佳:剑术差距 -15 级 │
│ 累计元经验:剑术 3200 (+800 本世) │
│ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ 新解锁词条:长寿(活到80岁解锁) │
│ │
│ [进入轮回] │
└─────────────────────────────────────────────────────┘
```
---
## 七、内容规划(MVP阶段)
### 7.1 MVP职业(10个)
**基础职业(4个)**
- 混混(短线,10级)
- 磨刀学徒(短线,10级)
- 农夫(短线,10级)
- 书童(短线,10级)
**进阶职业(4个)**
- 红棍(混混 Lv.10 解锁,短线,15级)
- 打刀下手(磨刀学徒 Lv.10 解锁,短线,15级)
- 猎户(农夫 Lv.10 解锁,短线,15级)
- 秀才(书童 Lv.10 解锁,中线,30级)
**高阶职业(2个)**
- 香主(红棍 Lv.15 解锁,中线,30级)
- 剑修(打刀下手 Lv.15 解锁,长线,50级)
### 7.2 MVP事件(30个)
- 幼年事件:5个
- 少年事件:5个
- 成年事件:10个
- 通用日常事件:10个
### 7.3 MVP词条(20个)
- 身份词条:5个
- 天赋词条:8个
- 携带物词条:4个
- 诅咒词条:3个
### 7.4 MVP NPC5个)
- 老乞丐(触发侠客路线)
- 道士(触发修仙路线)
- 张三(普通村民,有完整事件线)
- 李四(商人,可交易/赠送)
- 王五(敌对NPC,可能追杀玩家)
---
## 八、后续扩展方向
1. **多人/异步交互**:可以看到其他玩家的轮回记录,学习他们的选择
2. **MOD系统**:允许玩家自定义事件、职业、词条
3. **成就系统**Steam/平台成就集成
4. **可视化家族树**:显示多世的血缘/师承关系
5. **AI生成事件**:用LLM根据玩家历史生成个性化事件
+341
View File
@@ -0,0 +1,341 @@
# 轮回录 - 游戏理论审核报告
## 审核方法
- MDA 框架(Mechanics-Dynamics-Aesthetics
- 心流理论(Flow Channel
- 斯金纳箱 / 可变奖励系统
- 反馈密度分析
- 长期留存模型
- 成瘾机制审计
---
## 一、MDA 框架分析
### 1.1 Mechanics(机制层)
**当前设计的机制清单**
| 机制 | 深度 | 风险等级 |
|---|---|---|
| 按天推进 + 加速 | 中等 | 低 |
| 事件池(条件匹配 + 加权随机) | 高 | 中 |
| 标记系统(防逻辑错误) | 高 | 低 |
| 职业晋升链(解锁制) | 高 | 中 |
| 元经验(对数加成) | 中等 | 低 |
| 能力链(等级解锁技能) | 高 | 中 |
| 词条系统(随机特性池) | 高 | **高** |
| NPC事件线 | 高 | **高** |
| 三相外敌 + 隐藏第四相 | 高 | 中 |
| 10秒选择倒计时 | 低 | 低 |
| 死亡结算 + 历史对比 | 中等 | 低 |
**机制层风险评估**
**词条系统(风险:高)**
- 20个词条对于MVP来说数量偏少,重生3-4次后玩家就会感到"又在抽同样的词条"
- 建议MVP至少40-60个词条,或引入"词条组合效果"增加多样性
**NPC系统(风险:高)**
- 5个NPC × 每人一个事件线 = 极浅的叙事网络
- 如果NPC只是"背景板+偶尔送礼",玩家很快就会无视他们
- 建议至少有一个"深度NPC"(比如老乞丐),贯穿多世,每次遇到都有新对话
### 1.2 Dynamics(动态层)
**预期的玩家行为模式**
```
早期(1-10世):探索期
├── 行为:尝试不同路线,收集词条,摸清事件规律
├── 反馈:高频解锁(新职业、新词条、新事件)
└── 风险:如果前期解锁太慢,玩家会在第3-4世就放弃
中期(11-30世):优化期
├── 行为:针对特定路线优化,追求历史最佳记录
├── 反馈:元经验带来的明显加速,"这一世前期快多了"
└── 风险:如果优化空间太小(路线太线性),玩家会感到"换汤不换药"
后期(31-100世):终局期
├── 行为:对抗三相外敌,解锁隐藏第四相
├── 反馈:世界规则改变(道统),"我的门派存在于这个世界"
└── 风险:如果终局目标太遥远且没有中间里程碑,玩家会迷失
```
**动态层问题识别**
**问题1:探索期的"首因效应"陷阱**
MVP只有10个职业、30个事件。玩家在第3-4世就会看到重复内容。心理学中的**首因效应**告诉我们:玩家对前5小时的体验印象最深。如果前5小时都在重复,留存率会暴跌。
**修复建议**
- 前3世强制走不同路线(通过词条权重调整)
- 引入"首次遭遇加成":第一次看到某个事件时奖励翻倍
**问题2:优化期的"局部最优"困境**
玩家可能会发现某条路线(比如混混→红棍→香主)性价比最高,然后每世都走同一条路线。这违背了"无限试错"的设计目标。
**修复建议**
- 引入"路线疲劳":同一职业连走3世,元经验收益递减
- 引入"跨界加成":同时练两个职业,解锁combo技能
**问题3:终局期的"空窗期"焦虑**
从第31世到解锁第四相,可能需要20-30世的纯积累。这段时间玩家没有明确的短期目标。
**修复建议**
- 每10世设置一个"纪元事件"(如"第10世:天魔第一次入侵")
- 引入"世界进度可视化":让玩家看到三相的具体变化
### 1.3 Aesthetics(体验层)
**设计意图的体验**
| 体验 | 实现程度 | 评价 |
|---|---|---|
| **发现感**Sense of Discovery | ★★★★☆ | 随机词条 + 事件链提供充足的发现空间 |
| **成长感**Sense of Growth | ★★★★★ | 元经验 + 历史对比提供强烈的跨轮成长反馈 |
| **紧张感**Tension) | ★★☆☆☆ | 10秒倒计时和死亡机制有紧张感,但密度太低 |
| **叙事感**Narrative | ★★★☆☆ | 事件链和NPC提供叙事,但MVP内容量不足 |
| **掌控感**(Agency) | ★★★★☆ | 选择事件 + 硬门槛让玩家感到"我知道自己在做什么" |
| **社交感**(Social) | ★☆☆☆☆ | 目前完全没有社交元素 |
**体验层最大缺口:紧张感**
当前设计中,玩家每世只有 5~15 个选择事件,且 10 秒倒计时对于"加速模式"下的玩家来说几乎无感(他们可能在喝水/看手机)。
**修复建议**
- 引入"突发事件":加速时概率触发紧急事件,强制暂停
- 引入"倒计时危机":某些选择事件如果选错,不是立即死亡,而是触发"危机链"(如"你中了毒,接下来30天每天体质-1,必须找到解药")
---
## 二、心流通道分析
### 2.1 心流匹配图
```
挑战
│ ★ 终局对抗三相
│ ★
│ ★
│ ★ 解锁隐藏第四相
│ ★
│ ★ 对抗外敌事件
│ ★ 选择事件(高风险)
│★ 日常事件(低风险)
└──────────────────────────→ 技能(玩家经验)
新手 熟练 专家
```
**分析**
- 早期(新手):日常事件太简单,选择事件难度跳跃太大。存在"无聊期"和"焦虑期"的交替。
- 中期(熟练):外敌事件提供适度挑战,但日常事件仍然太简单。
- 后期(专家):终局挑战足够,但到达终局的过程太长。
**修复建议**
- 引入"自适应难度":根据玩家历史最佳表现调整事件难度
- 引入"挑战模式":玩家可以主动选择更高风险的事件链,换取更高回报
### 2.2 死亡机制的心流影响
当前死亡是"自然衰减"(年龄越大概率越高)+ "外敌阈值"。
**问题**:死亡太"平淡"。玩家活到80岁,看到"寿终正寝",没有任何情绪冲击。
**修复建议(借鉴《暗黑地牢》的Stress系统)**
- 引入"危机感"数值:随着年龄/外敌增长,玩家每天的危机感上升
- 危机感过高时:事件池偏向负面事件,选择事件选项减少
- 危机感可以通过特定事件降低(如"归隐田园")
---
## 三、斯金纳箱 / 可变奖励分析
### 3.1 奖励类型分布
| 奖励类型 | 频率 | 可预测性 | 成瘾性 |
|---|---|---|---|
| 每天经验增长 | 极高(每天) | 完全可预测 | 低 |
| 职业升级 | 中等(几天一次) | 可预测 | 中 |
| 文本事件 | 高(每天多次) | 随机 | 中 |
| 选择事件 | 中等(每年几次) | 半随机 | 高 |
| 词条解锁 | 低(每几世一次) | 随机 | **极高** |
| 新职业解锁 | 很低(几十世一次) | 条件驱动 | 高 |
| 外敌击退 | 极低 | 长期目标 | 中 |
### 3.2 成瘾机制审计
**正面成瘾设计**
- ✅ 词条系统 = 抽卡机制(可变奖励)
- ✅ 历史对比 = 社交比较("我比上一世强了")
- ✅ 元经验 = 沉没成本("我已经练了20世了,不能放弃")
**负面成瘾风险**
- ⚠️ 词条池太小 → 很快变成"重复劳动"而非"期待惊喜"
- ⚠️ 日常事件太频繁 → 玩家可能"挂机刷",失去主动参与感
- ⚠️ 没有"保底机制" → 玩家可能几十世都抽不到想要的词条,产生挫败感
**修复建议**
- 引入"词条保底":每10世必出一个未解锁词条
- 引入"词条许愿":可以指定1个词条,抽到它的概率翻倍(但其他词条概率降低)
---
## 四、反馈密度分析
### 4.1 每世内的反馈密度
假设一局人生60年:
| 年龄段 | 天数 | 文本事件 | 选择事件 | 反馈/天 |
|---|---|---|---|---|
| 幼年 | 7年 | ~150个 | 3个 | 0.06 |
| 少年 | 9年 | ~120个 | 4个 | 0.04 |
| 青年 | 10年 | ~70个 | 3个 | 0.02 |
| 壮年 | 15年 | ~60个 | 3个 | 0.01 |
| 中年 | 20年 | ~60个 | 2个 | 0.008 |
| 老年 | 10年 | ~70个 | 3个 | 0.02 |
**问题**:壮年/中年期的反馈密度太低(每天0.01次事件)。加速模式下,玩家每分钟可能只看到1-2个事件。
**修复建议**
- 引入"里程碑事件":每5年强制触发一个有纪念意义的事件(如"你第一次独立完成了..."
- 引入"能力突破事件":每升10级触发一个特殊事件,提供强烈的正反馈
### 4.2 跨世反馈密度
| 轮回 | 预期反馈 | 评价 |
|---|---|---|
| 1-3 | 大量新内容解锁 | 极高密度,容易上头 |
| 4-10 | 新词条 + 元经验加速 | 高密度,持续吸引 |
| 11-20 | 元经验 + 偶尔新词条 | 中密度,需要事件多样性支撑 |
| 21-50 | 元经验 + 外敌推进 | 低密度,风险期 |
| 51+ | 终局目标可见 | 密度回升,但需要前期不流失 |
**关键风险期:第21-50世**
这段时间玩家已经解锁了大部分内容,日常体验变成"刷经验 → 死亡 → 刷经验"。如果没有任何新的刺激,玩家会在这个阶段流失。
**修复建议**
- 引入"世代事件":每10世触发一个独特的世界观事件(如"第30世:天魔第一次降临")
- 引入"传承系统":玩家可以给下一世留下"遗言"或"遗志",下一世看到时有特殊加成
---
## 五、长期留存模型
### 5.1 留存曲线预测
```
留存率
100% ┤★
80% ┤ ★
60% ┤ ★★
40% ┤ ★★★
20% ┤ ★★★★
10% ┤ ★★★★★★
5% ┤ ★★★★★★★★
└────────────────────────
D1 D3 D7 D14 D30 D60 D90
```
**预测**
- D1 留存:~80%(首日体验丰富,解锁大量内容)
- D7 留存:~40%(第3-4世后内容重复感出现)
- D30 留存:~15%(如果没有足够的世代事件,玩家流失)
- D90 留存:~5%(只有核心玩家坚持到终局)
### 5.2 提升留存的设计建议
**短期(D1-D7**
- 前3世强制体验3条不同路线
- 每日登录奖励(解锁特殊词条)
- "上一世的你留下了..."(每次登录时的叙事钩子)
**中期(D7-D30**
- 每周挑战("本周只能走武道路线",完成奖励词条)
- 玩家排行榜("你的第10世在历史排名中第X")
- 事件编辑器(让玩家自己写事件,被点赞多的加入官方池)
**长期(D30+**
- 终局赛季制(每赛季重置外敌进度,但保留词条池)
- 多人异步(看到其他玩家的"墓碑"和遗言)
- MOD支持(社区创作内容)
---
## 六、关键设计矛盾与修复方案
### 矛盾1"无限试错" vs "路线疲劳"
**问题**:玩家发现最优路线后,会反复走同一条路。
**修复**
- 同职业连续3世,元经验收益 ×0.7
- 新职业首世,元经验收益 ×2.0
- 引入"跨界技能":两个职业都达标后解锁组合技能
### 矛盾2"每天推进" vs "信息过载"
**问题**:按天推进意味着每年有365个tick,事件密度太高会 overwhelm 玩家,太低会显得空洞。
**修复**
- 引入"事件批次":每天不触发事件,而是累积"事件概率",达到阈值时一次性触发
- 加速模式下,事件自动聚合("这一周发生了3件事..."
### 矛盾3"随机词条" vs "策略规划"
**问题**:如果词条完全随机,玩家无法做长期规划;如果词条可预测,就失去了发现感。
**修复**
- 引入"词条倾向":根据上一世的选择,某些词条概率提升(如上一世当混混,"江湖中人"词条概率+50%
- 引入"词条预览":重生前可以看到"下一世有30%概率抽到剑骨"
### 矛盾4"硬门槛" vs "戏剧性"
**问题**:纯硬门槛缺少"差一点就成功"的戏剧性。
**修复**
- 硬门槛用于"不可能事件"(如悟性<3不可能修仙)
- 门槛内引入"浮动奖励":属性刚好够门槛时,奖励翻倍;远超门槛时,奖励正常
---
## 七、总结与优先级
### 必须修复(MVP阶段)
1. **词条池扩大**:MVP至少40-60个词条,或引入词条组合
2. **事件密度优化**:壮年/中年期每天至少有1个可见事件
3. **死亡冲击力**:引入"危机感"系统,让死亡前有预兆
4. **首因效应**:前3世强制引导玩家走不同路线
### 应该修复(EA阶段)
5. **世代事件**:每10世一个独特的世界观事件
6. **词条保底**:每10世必出1个未解锁词条
7. **NPC深度**:至少1个贯穿多世的深度NPC
8. **里程碑事件**:每5年/每10级一个强反馈事件
### 可以延后(正式版)
9. 社交/异步交互
10. MOD系统
11. AI生成事件
12. 赛季制
---
## 八、最终评分
| 维度 | 评分 | 说明 |
|---|---|---|
| 核心循环完整性 | 8/10 | 循环清晰,但终局目标太遥远 |
| 反馈密度 | 6/10 | 中后期密度不足,容易疲劳 |
| 随机性平衡 | 7/10 | 硬门槛+随机词条是好的,但词条池太小 |
| 长期留存潜力 | 6/10 | 50-100世的目标需要更多中间里程碑支撑 |
| 叙事深度 | 5/10 | MVP内容量不足以支撑叙事 |
| 成瘾性设计 | 7/10 | 抽卡式词条+历史对比是好设计,但需要保底 |
| 心流匹配 | 6/10 | 早期和晚期匹配好,中期存在空窗期 |
| **综合评分** | **6.4/10** | 基础框架扎实,但需要大量内容填充和中期体验优化 |
**结论**:设计方向正确,核心循环有潜力。MVP阶段最大的风险是"内容量不足导致早期流失"和"中期空窗期导致玩家疲劳"。建议优先扩大词条池、增加事件密度、引入世代事件。
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,496 @@
# 轮回录 - 配置驱动重构设计文档
## 目标
将《轮回录》从硬编码内容重构为**纯配置驱动框架**。
- `engine/` 中零游戏内容字符串,全部从 `config/*.json` 加载
- 职业系统改为**网状并行非转职**结构
- 外族入侵从进度条改为**年龄触发剧情事件**
- 新增银两经济 + 系统商铺(3 Tab)
- 前几世不可能破局,通过多轮积累解锁后期内容
---
## 一、目录结构
```
rebirth-incremental/
├── index.html
├── src/
│ ├── main.js # 入口:加载配置 → 初始化引擎 → 启动
│ ├── engine/ # 纯框架,零硬编码内容
│ │ ├── state.js # 状态管理 + 序列化
│ │ ├── tickLoop.js # 游戏主循环(speed控制、每日结算)
│ │ ├── conditionEvaluator.js # 统一条件解析器(所有系统共用)
│ │ ├── effectApplier.js # 统一效果执行器(所有系统共用)
│ │ ├── careerEngine.js # 职业网引擎(解锁判定、经验计算、收入结算)
│ │ ├── eventEngine.js # 事件引擎(触发判定、选择处理)
│ │ ├── shopEngine.js # 商铺引擎(购买、生效、重置逻辑)
│ │ ├── moneySystem.js # 银两系统(收入、支出、余额)
│ │ ├── invasionEngine.js # 外族入侵引擎(年龄触发、破局判定)
│ │ └── deathEngine.js # 死亡与重生引擎
│ ├── config/ # 所有内容JSON
│ │ ├── careers.json # 职业定义(含解锁条件、收入、经验曲线)
│ │ ├── events.json # 普通事件 + 入侵事件
│ │ ├── shop.json # 物品 + Buff + 神器
│ │ ├── identities.json # 开局身份
│ │ └── talents.json # 词条
│ └── ui/
│ ├── renderer.js # 主渲染调度
│ └── components/ # UI组件(后续拆分)
```
---
## 二、状态结构(state.js
```javascript
state = {
// 时间
day: 0,
year: 0,
age: 0,
reincarnation: 0,
// 玩家基础
alive: true,
identity: null, // 开局身份ID
stats: {
body: 0, // 体质
wisdom: 0, // 悟性
charm: 0, // 魅力
destiny: 0, // 天命
business: 0, // 经商
intelligence: 0 // 智力
},
talents: [], // 本轮抽到的词条ID数组
// 职业网(并行,每个职业独立等级)
careers: {
// "student": { level: 150, exp: 1234 }
},
// 经济
money: 0,
// 商铺(本轮有效)
shopItems: {}, // { itemId: quantity }
activeBuffs: {}, // { buffId: { expiresDay } }
// 神器(跨轮永久)
artifacts: {}, // { artifactId: level }
// 世界状态
worldFlags: {},
npcs: {},
// 入侵状态
invasionsTriggered: {
military_40: false,
spiritual_45: false,
political_50: false,
final_60: false
},
breakthroughRoute: null, // 'general' | 'minister' | 'immortal' | null
// 运行状态
speed: 0,
paused: false,
logs: [],
triggeredEvents: new Set(),
// 跨轮永久积累
metaExp: {}, // { careerId: amount }
memories: [],
unlockedEntries: new Set(),
history: [],
unlockedShopPool: new Set(), // 已解锁的商铺物品资格(永久)
};
```
---
## 三、6属性加成系统
每点属性提供 **1%** 对应加成,**上限100%**(即属性最多生效到100点)。
| 属性 | 加成目标 |
|------|---------|
| 体质(body) | 军事/武将类职业经验获取 |
| 悟性(wisdom) | 修仙/书生类职业经验获取 |
| 魅力(charm) | 江湖/社交类职业经验获取 |
| 天命(destiny) | 事件收益/随机暴击概率 |
| 经商(business) | 银两收入 |
| 智力(intelligence) | 政治/策略类职业经验获取 |
**计算公式:**
```
加成倍率 = 1 + min(属性值, 100) * 0.01
职业经验 = 基础每日经验 * (1 + 对应属性 * 0.01) * 元经验倍率 * 其他倍率
银两收入 = 基础收入 * (1 + 经商 * 0.01) * 其他倍率
```
---
## 四、职业系统
### 4.1 核心规则
- **并行非转职**:所有已解锁职业同时存在,各自独立等级,不是替换关系
- **解锁驱动**:满足条件后新职业加入可练池,旧职业继续升级
- **收入/支出**:每日tick时所有已解锁职业的收入求和(负数为支出)
- **经验曲线**`needExp = base * factor^level`,无硬上限,等级可无限升
- **属性加成**:职业JSON中标记`type`,对应属性提供经验加成
### 4.2 careers.json 格式
```json
{
"careers": [
{
"id": "student",
"name": "学童",
"type": "scholar",
"unlockConditions": [
{ "type": "age", "op": ">=", "value": 6 }
],
"dailyIncome": 1,
"expCurve": { "base": 10, "factor": 1.15 }
},
{
"id": "book_boy",
"name": "书童",
"type": "scholar",
"unlockConditions": [
{ "type": "careerLevel", "careerId": "student", "op": ">=", "value": 100 }
],
"dailyIncome": 2,
"expCurve": { "base": 50, "factor": 1.2 }
},
{
"id": "scholar",
"name": "书生",
"type": "scholar",
"unlockConditions": [
{ "type": "careerLevel", "careerId": "student", "op": ">=", "value": 250 },
{ "type": "age", "op": ">=", "value": 14 }
],
"dailyIncome": -3,
"expCurve": { "base": 100, "factor": 1.25 }
}
]
}
```
### 4.3 职业type与属性映射
| type | 对应属性 |
|------|---------|
| military | 体质 |
| scholar | 悟性 |
| jianghu | 魅力 |
| political | 智力 |
| immortal | 悟性 |
| business | 经商 |
---
## 五、系统商铺(3 Tab
### 5.1 设计原则
| Tab | 是否重置 | 说明 |
|-----|---------|------|
| 物品 | 每轮清零 | 属性增强、解锁职业资格(如道碟)、消耗品 |
| Buff | 每轮清零 | 属性加成、经验倍率加成,限时或永久(本轮) |
| 神器 | 跨轮永久 | 属性+特殊效果,可用银两升级,需解锁资格后才能升级 |
### 5.2 shop.json 格式
```json
{
"items": [
{
"id": "taoist_license",
"name": "道碟",
"tab": "item",
"cost": 500,
"unlockConditions": [
{ "type": "age", "op": ">=", "value": 10 },
{ "type": "or", "conditions": [
{ "type": "identity", "value": "taoist_orphan" },
{ "type": "worldFlag", "flag": "met_taoist_priest", "value": true }
]}
],
"effects": [
{ "type": "unlockCareer", "careerId": "taoist_priest" },
{ "type": "setFlag", "flag": "has_taoist_license", "value": true }
],
"resetOnRebirth": true
}
],
"buffs": [
{
"id": "clever_brush",
"name": "妙笔生花",
"tab": "buff",
"cost": 200,
"unlockConditions": [],
"effects": [
{ "type": "addStat", "stat": "wisdom", "value": 5 }
],
"resetOnRebirth": true
}
],
"artifacts": [
{
"id": "immortal_sword",
"name": "仙剑",
"tab": "artifact",
"baseCost": 1000,
"costGrowth": 1.5,
"unlockConditions": [
{ "type": "careerLevel", "careerId": "sword_immortal", "op": ">=", "value": 100 }
],
"effects": [
{ "type": "addStat", "stat": "body", "value": 10 },
{ "type": "multiplyExp", "careerType": "immortal", "value": 1.2 }
],
"resetOnRebirth": false,
"maxLevel": 10
}
]
}
```
### 5.3 神器升级公式
```
升级所需银两 = baseCost * costGrowth^(当前等级)
例:baseCost=1000, costGrowth=1.5
1→2级:1000
2→3级:1500
3→4级:2250
```
---
## 六、统一条件与效果系统
### 6.1 条件格式(conditionEvaluator.js
所有条件统一JSON格式,引擎提供 `evaluateCondition(condition, state)` → boolean。
```json
// 基础条件
{ "type": "age", "op": ">=", "value": 14 }
{ "type": "careerLevel", "careerId": "student", "op": ">=", "value": 100 }
{ "type": "stat", "stat": "wisdom", "op": ">=", "value": 20 }
{ "type": "money", "op": ">=", "value": 500 }
{ "type": "hasItem", "itemId": "taoist_license" }
{ "type": "identity", "value": "taoist_orphan" }
{ "type": "worldFlag", "flag": "met_taoist", "value": true }
{ "type": "reincarnation", "op": ">=", "value": 3 }
{ "type": "hasArtifact", "artifactId": "immortal_sword", "op": ">=", "value": 3 }
// 组合条件
{ "type": "and", "conditions": [...] }
{ "type": "or", "conditions": [...] }
{ "type": "not", "condition": {...} }
```
### 6.2 效果格式(effectApplier.js
所有效果统一JSON格式,引擎提供 `applyEffect(effect, state)` → 修改state。
```json
{ "type": "addMoney", "value": 100 }
{ "type": "addExp", "careerId": "student", "value": 10 }
{ "type": "addStat", "stat": "wisdom", "value": 5 }
{ "type": "setFlag", "flag": "x", "value": true }
{ "type": "unlockCareer", "careerId": "taoist_priest" }
{ "type": "addItem", "itemId": "taoist_license" }
{ "type": "addBuff", "buffId": "clever_brush", "duration": -1 }
{ "type": "upgradeArtifact", "artifactId": "immortal_sword" }
{ "type": "addLog", "text": "...", "logType": "major" }
{ "type": "die", "reason": "..." }
```
---
## 七、事件系统
### 7.1 事件分类
| 类型 | 触发方式 | 说明 |
|------|---------|------|
| random | 权重随机池 | 普通日常事件,按密度触发 |
| age | 到达指定年龄 | 入侵事件、关键剧情 |
| flag | 世界Flag变化 | 连锁剧情 |
### 7.2 events.json 格式
```json
{
"events": [
{
"id": "daily_training",
"trigger": { "type": "random", "pool": "normal", "weight": 100 },
"text": "你进行了日常训练...",
"effects": [
{ "type": "addExp", "careerId": "student", "value": 5 }
]
},
{
"id": "invasion_military_40",
"trigger": { "type": "age", "value": 40 },
"isChoice": true,
"title": "蛮族入侵",
"text": "北方蛮族大举南下...",
"choices": [
{
"text": "组织乡勇抵抗",
"requirements": [
{ "type": "careerLevel", "careerId": "soldier", "op": ">=", "value": 50 }
],
"effects": [
{ "type": "setFlag", "flag": "invasion_military_resisted", "value": true },
{ "type": "unlockCareer", "careerId": "general" }
]
},
{
"text": "逃往南方",
"effects": [
{ "type": "addStat", "stat": "body", "value": -2 }
]
}
]
}
]
}
```
---
## 八、外族入侵与破局系统
### 8.1 入侵事件时间表
| 年龄 | 事件 | 入侵类型 |
|------|------|---------|
| 40岁 | 第一次入侵 | 军事(蛮族) |
| 45岁 | 第二次入侵 | 精神(天魔) |
| 50岁 | 第三次入侵 | 政治(王朝崩坏) |
| 60岁 | 最终危机 | 三相汇聚,必须破局 |
### 8.2 破局路线
破局路线是极后期的职业系统,需要多世积累才能达到解锁条件:
| 破局路线 | 前置条件示例 | 对应入侵 |
|---------|------------|---------|
| 将军破局 | 士兵职业500级 + 体质>80 + 神器"虎符"Lv5 | 军事 |
| 宰相破局 | 政治职业500级 + 智力>80 + 神器"玉玺"Lv5 | 政治 |
| 修仙破局 | 修仙职业500级 + 悟性>80 + 神器"飞剑"Lv5 | 精神 |
**前几世不可能破局**——即使运气极好,属性和职业等级也远远不够。
---
## 九、游戏循环(每tick
```
for each day:
1. 年龄增长检查(day % 365 === 0
2. 银两结算(所有已解锁职业的 dailyIncome 求和 * 经商加成)
3. 职业经验结算(所有已解锁职业的每日经验 * 对应属性加成 * 元经验倍率)
4. 检查职业解锁(遍历careers.json,条件满足则加入state.careers
5. 检查年龄触发事件(40/45/50/60岁入侵,只触发一次)
6. 随机事件触发(按密度池抽取)
7. 检查死亡条件(年龄、属性、入侵后果)
8. 渲染UI
```
---
## 十、前几世节奏设计
| 轮次 | 目标 | 解锁内容 |
|------|------|---------|
| 第1世 | 熟悉系统,解锁基础职业,赚少量银两买小Buff | 学童、农夫等 |
| 第2世 | 词条/记忆开局+属性,解锁中级职业,买第一件神器 | 书童、猎户等 |
| 第3世 | 神器等级提升,属性更高,解锁高级职业 | 书生、侠客等 |
| 第4世+ | 接近破局门槛,尝试解锁破局职业 | 将军、宰相、修仙 |
---
## 十一、存档与跨轮继承
### 11.1 每轮重置
- 年龄、天数、银两、世界Flag、NPC关系
- 已购买的物品和Buff
- 职业等级(但元经验保留,下轮加速)
### 11.2 跨轮永久保留
- 元经验(metaExp
- 记忆(memories
- 已解锁词条池(unlockedEntries
- 神器及等级(artifacts
- 历史记录(history
- 解锁的商铺资格(unlockedShopPool
---
## 十二、UI 面板
| 面板 | 内容 |
|------|------|
| 顶部信息 | 第N世、年月日、年龄、银两、6属性值 |
| 入侵进度 | 三次入侵触发状态、破局路线 |
| 控制按钮 | 速度、暂停、存档、读档 |
| 事件面板 | 当前事件标题、文本、倒计时、选择按钮 |
| 商铺面板 | 3 Tab(物品/Buff/神器),显示解锁状态和价格 |
| 职业面板 | 所有已解锁职业列表,显示等级、经验条、每日收入 |
| 日志流 | 事件记录 |
| 词条面板 | 本轮抽到的词条 |
| 神器面板 | 已拥有神器及等级 |
| 死亡结算 | 死亡原因、本轮成就、新解锁内容、历史对比 |
---
## 十三、实现优先级
1. **Phase 1:框架搭建**
- 新建目录结构
- 实现 `conditionEvaluator.js` + `effectApplier.js`
- 实现 `state.js` + 序列化
- 实现 `tickLoop.js`
2. **Phase 2:职业网 + 经济**
- 实现 `careerEngine.js` + `moneySystem.js`
- 编写 `careers.json` 框架(少量示例职业)
3. **Phase 3:事件系统**
- 实现 `eventEngine.js`
- 编写 `events.json` 框架
4. **Phase 4:商铺系统**
- 实现 `shopEngine.js`
- 编写 `shop.json` 框架(3 Tab示例)
5. **Phase 5:入侵与破局**
- 实现 `invasionEngine.js`
- 编写入侵事件JSON
6. **Phase 6:死亡重生 + 跨轮继承**
- 实现 `deathEngine.js`
- 完成跨轮状态过滤
7. **Phase 7UI**
- 重写 `renderer.js`
- 新增商铺面板、神器面板
8. **Phase 8:内容填充**
- 大量编写JSON内容(职业、事件、商铺、身份、词条)
+704 -339
View File
File diff suppressed because it is too large Load Diff
+933
View File
@@ -0,0 +1,933 @@
{
"name": "rebirth-incremental",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"puppeteer-core": "^24.43.1"
}
},
"node_modules/@puppeteer/browsers": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz",
"integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.3",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.4",
"tar-fs": "^3.1.1",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/cjs/main-cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
"license": "MIT",
"optional": true,
"dependencies": {
"undici-types": "~7.21.0"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/b4a": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
"integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
"integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
"integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz",
"integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==",
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.25.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-abort-controller": "*",
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
},
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz",
"integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/basic-ftp": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/chromium-bidi": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz",
"integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1608973",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz",
"integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==",
"license": "BSD-3-Clause"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/netmask": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz",
"integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
"license": "MIT",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.6",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"license": "MIT",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.6",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.1.0",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/puppeteer-core": {
"version": "24.43.1",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz",
"integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.13.2",
"chromium-bidi": "14.0.0",
"debug": "^4.4.3",
"devtools-protocol": "0.0.1608973",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.1",
"ws": "^8.20.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/semver": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.1.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamx": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tar-fs": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
"integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
"integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typed-query-selector": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
"license": "MIT",
"optional": true
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz",
"integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==",
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+1
View File
@@ -0,0 +1 @@
{"type":"module","dependencies":{"puppeteer-core":"^24.43.1"}}
+58
View File
@@ -0,0 +1,58 @@
import http from 'http';
import fs from 'fs';
import path from 'path';
import { networkInterfaces } from 'os';
const PORT = process.env.PORT || 8765;
const HOST = '0.0.0.0';
const mimeTypes = {
'.html': 'text/html',
'.js': 'application/javascript',
'.json': 'application/json',
'.css': 'text/css',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.mp3': 'audio/mpeg',
};
function getLocalIPs() {
const nets = networkInterfaces();
const ips = [];
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
if (net.family === 'IPv4' && !net.internal) {
ips.push(net.address);
}
}
}
return ips;
}
const server = http.createServer((req, res) => {
let filePath = '.' + (req.url === '/' ? '/index.html' : req.url);
const ext = path.extname(filePath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
fs.readFile(filePath, (err, content) => {
if (err) {
res.writeHead(404);
res.end('Not found');
return;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
});
});
server.listen(PORT, HOST, () => {
console.log(`\n 轮回录 服务器已启动`);
console.log(` ========================`);
console.log(` 本机访问: http://localhost:${PORT}`);
const ips = getLocalIPs();
for (const ip of ips) {
console.log(` 局域网访问: http://${ip}:${PORT}`);
}
console.log(` ========================\n`);
});
+418 -63
View File
@@ -4,32 +4,17 @@
"id": "student", "id": "student",
"name": "学童", "name": "学童",
"type": "scholar", "type": "scholar",
"unlockConditions": { "unlockConditions": { "type": "age", "op": ">=", "value": 6 },
"type": "age",
"op": ">=",
"value": 6
},
"dailyIncome": 1, "dailyIncome": 1,
"expCurve": { "expCurve": { "base": 10, "factor": 1.15 }
"base": 10,
"factor": 1.15
}
}, },
{ {
"id": "book_boy", "id": "book_boy",
"name": "书童", "name": "书童",
"type": "scholar", "type": "scholar",
"unlockConditions": { "unlockConditions": { "type": "careerLevel", "op": ">=", "careerId": "student", "value": 100 },
"type": "careerLevel",
"op": ">=",
"careerId": "student",
"value": 100
},
"dailyIncome": 2, "dailyIncome": 2,
"expCurve": { "expCurve": { "base": 50, "factor": 1.2 }
"base": 50,
"factor": 1.2
}
}, },
{ {
"id": "scholar", "id": "scholar",
@@ -38,25 +23,119 @@
"unlockConditions": { "unlockConditions": {
"type": "and", "type": "and",
"conditions": [ "conditions": [
{ { "type": "careerLevel", "op": ">=", "careerId": "student", "value": 250 },
"type": "careerLevel", { "type": "age", "op": ">=", "value": 14 }
"op": ">=",
"careerId": "student",
"value": 250
},
{
"type": "age",
"op": ">=",
"value": 14
}
] ]
}, },
"dailyIncome": -3, "dailyIncome": 3,
"expCurve": { "expCurve": { "base": 100, "factor": 1.25 }
"base": 100,
"factor": 1.25
}
}, },
{
"id": "xiucai",
"name": "秀才",
"type": "scholar",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "scholar", "value": 500 },
{ "type": "age", "op": ">=", "value": 20 },
{ "type": "stat", "op": ">=", "stat": "wisdom", "value": 30 }
]
},
"dailyIncome": 5,
"expCurve": { "base": 200, "factor": 1.28 }
},
{
"id": "juren",
"name": "举人",
"type": "scholar",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "xiucai", "value": 800 },
{ "type": "age", "op": ">=", "value": 25 },
{ "type": "stat", "op": ">=", "stat": "wisdom", "value": 50 }
]
},
"dailyIncome": 8,
"expCurve": { "base": 400, "factor": 1.3 }
},
{
"id": "jinshi",
"name": "进士",
"type": "scholar",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "juren", "value": 1000 },
{ "type": "age", "op": ">=", "value": 30 },
{ "type": "stat", "op": ">=", "stat": "wisdom", "value": 80 }
]
},
"dailyIncome": 12,
"expCurve": { "base": 800, "factor": 1.32 }
},
{
"id": "hanlin",
"name": "翰林",
"type": "scholar",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "jinshi", "value": 1500 },
{ "type": "age", "op": ">=", "value": 35 },
{ "type": "stat", "op": ">=", "stat": "intelligence", "value": 60 }
]
},
"dailyIncome": 20,
"expCurve": { "base": 1500, "factor": 1.35 }
},
{
"id": "shilang",
"name": "侍郎",
"type": "political",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "hanlin", "value": 2000 },
{ "type": "age", "op": ">=", "value": 40 },
{ "type": "stat", "op": ">=", "stat": "intelligence", "value": 80 }
]
},
"dailyIncome": 35,
"expCurve": { "base": 2500, "factor": 1.38 }
},
{
"id": "shangshu",
"name": "尚书",
"type": "political",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "shilang", "value": 3000 },
{ "type": "age", "op": ">=", "value": 45 },
{ "type": "stat", "op": ">=", "stat": "intelligence", "value": 100 }
]
},
"dailyIncome": 60,
"expCurve": { "base": 4000, "factor": 1.4 }
},
{
"id": "chancellor",
"name": "宰相",
"type": "political",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "shangshu", "value": 5000 },
{ "type": "age", "op": ">=", "value": 50 },
{ "type": "worldFlag", "flag": "resisted_political_invasion", "value": true }
]
},
"dailyIncome": 100,
"expCurve": { "base": 8000, "factor": 1.45 }
},
{ {
"id": "soldier", "id": "soldier",
"name": "士兵", "name": "士兵",
@@ -64,25 +143,95 @@
"unlockConditions": { "unlockConditions": {
"type": "and", "type": "and",
"conditions": [ "conditions": [
{ { "type": "age", "op": ">=", "value": 14 },
"type": "age", { "type": "stat", "op": ">=", "stat": "body", "value": 10 }
"op": ">=",
"value": 14
},
{
"type": "stat",
"op": ">=",
"stat": "body",
"value": 10
}
] ]
}, },
"dailyIncome": 2, "dailyIncome": 2,
"expCurve": { "expCurve": { "base": 20, "factor": 1.18 }
"base": 20,
"factor": 1.18
}
}, },
{
"id": "squad_leader",
"name": "伍长",
"type": "military",
"unlockConditions": { "type": "careerLevel", "op": ">=", "careerId": "soldier", "value": 100 },
"dailyIncome": 4,
"expCurve": { "base": 80, "factor": 1.22 }
},
{
"id": "centurion",
"name": "百夫长",
"type": "military",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "soldier", "value": 500 },
{ "type": "stat", "op": ">=", "stat": "body", "value": 30 }
]
},
"dailyIncome": 8,
"expCurve": { "base": 200, "factor": 1.25 }
},
{
"id": "commander",
"name": "千夫长",
"type": "military",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "centurion", "value": 800 },
{ "type": "stat", "op": ">=", "stat": "body", "value": 50 }
]
},
"dailyIncome": 15,
"expCurve": { "base": 500, "factor": 1.28 }
},
{
"id": "general",
"name": "将军",
"type": "military",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "commander", "value": 1000 },
{ "type": "age", "op": ">=", "value": 35 },
{ "type": "stat", "op": ">=", "stat": "body", "value": 80 }
]
},
"dailyIncome": 30,
"expCurve": { "base": 1000, "factor": 1.32 }
},
{
"id": "grand_general",
"name": "大将军",
"type": "military",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "general", "value": 2000 },
{ "type": "age", "op": ">=", "value": 40 },
{ "type": "stat", "op": ">=", "stat": "body", "value": 100 }
]
},
"dailyIncome": 50,
"expCurve": { "base": 2000, "factor": 1.35 }
},
{
"id": "marshal",
"name": "元帅",
"type": "military",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "grand_general", "value": 3000 },
{ "type": "age", "op": ">=", "value": 45 },
{ "type": "worldFlag", "flag": "resisted_military_invasion", "value": true }
]
},
"dailyIncome": 80,
"expCurve": { "base": 4000, "factor": 1.4 }
},
{ {
"id": "taoist_priest", "id": "taoist_priest",
"name": "道士", "name": "道士",
@@ -90,22 +239,228 @@
"unlockConditions": { "unlockConditions": {
"type": "and", "type": "and",
"conditions": [ "conditions": [
{ { "type": "age", "op": ">=", "value": 10 },
"type": "age", { "type": "hasItem", "itemId": "taoist_license" }
"op": ">=",
"value": 10
},
{
"type": "hasItem",
"itemId": "taoist_license"
}
] ]
}, },
"dailyIncome": 1, "dailyIncome": 1,
"expCurve": { "expCurve": { "base": 30, "factor": 1.22 }
"base": 30, },
"factor": 1.22 {
} "id": "dao_tong",
"name": "道童",
"type": "immortal",
"unlockConditions": { "type": "careerLevel", "op": ">=", "careerId": "taoist_priest", "value": 100 },
"dailyIncome": 2,
"expCurve": { "base": 100, "factor": 1.25 }
},
{
"id": "dao_zhang",
"name": "道长",
"type": "immortal",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "dao_tong", "value": 500 },
{ "type": "stat", "op": ">=", "stat": "wisdom", "value": 30 }
]
},
"dailyIncome": 4,
"expCurve": { "base": 300, "factor": 1.28 }
},
{
"id": "zhen_ren",
"name": "真人",
"type": "immortal",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "dao_zhang", "value": 1000 },
{ "type": "stat", "op": ">=", "stat": "wisdom", "value": 60 }
]
},
"dailyIncome": 8,
"expCurve": { "base": 800, "factor": 1.32 }
},
{
"id": "san_xian",
"name": "散仙",
"type": "immortal",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "zhen_ren", "value": 1500 },
{ "type": "age", "op": ">=", "value": 40 },
{ "type": "stat", "op": ">=", "stat": "destiny", "value": 50 }
]
},
"dailyIncome": 15,
"expCurve": { "base": 1500, "factor": 1.35 }
},
{
"id": "di_xian",
"name": "地仙",
"type": "immortal",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "san_xian", "value": 2000 },
{ "type": "age", "op": ">=", "value": 45 },
{ "type": "stat", "op": ">=", "stat": "wisdom", "value": 100 }
]
},
"dailyIncome": 30,
"expCurve": { "base": 3000, "factor": 1.38 }
},
{
"id": "tian_xian",
"name": "天仙",
"type": "immortal",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "di_xian", "value": 3000 },
{ "type": "age", "op": ">=", "value": 50 },
{ "type": "stat", "op": ">=", "stat": "destiny", "value": 80 }
]
},
"dailyIncome": 60,
"expCurve": { "base": 6000, "factor": 1.42 }
},
{
"id": "hoodlum",
"name": "混混",
"type": "jianghu",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "age", "op": ">=", "value": 10 },
{ "type": "stat", "op": ">=", "stat": "body", "value": 5 }
]
},
"dailyIncome": 1,
"expCurve": { "base": 15, "factor": 1.18 }
},
{
"id": "ranger",
"name": "游侠",
"type": "jianghu",
"unlockConditions": { "type": "careerLevel", "op": ">=", "careerId": "hoodlum", "value": 100 },
"dailyIncome": 3,
"expCurve": { "base": 60, "factor": 1.22 }
},
{
"id": "swordsman",
"name": "剑客",
"type": "jianghu",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "ranger", "value": 500 },
{ "type": "stat", "op": ">=", "stat": "body", "value": 30 },
{ "type": "stat", "op": ">=", "stat": "charm", "value": 20 }
]
},
"dailyIncome": 6,
"expCurve": { "base": 200, "factor": 1.26 }
},
{
"id": "hero",
"name": "大侠",
"type": "jianghu",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "swordsman", "value": 1000 },
{ "type": "stat", "op": ">=", "stat": "body", "value": 60 },
{ "type": "stat", "op": ">=", "stat": "charm", "value": 40 }
]
},
"dailyIncome": 12,
"expCurve": { "base": 600, "factor": 1.3 }
},
{
"id": "grandmaster",
"name": "宗师",
"type": "jianghu",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "hero", "value": 2000 },
{ "type": "age", "op": ">=", "value": 40 },
{ "type": "stat", "op": ">=", "stat": "body", "value": 100 },
{ "type": "stat", "op": ">=", "stat": "charm", "value": 60 }
]
},
"dailyIncome": 25,
"expCurve": { "base": 1500, "factor": 1.35 }
},
{
"id": "apprentice",
"name": "学徒",
"type": "business",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "age", "op": ">=", "value": 10 },
{ "type": "stat", "op": ">=", "stat": "business", "value": 5 }
]
},
"dailyIncome": 2,
"expCurve": { "base": 20, "factor": 1.18 }
},
{
"id": "merchant",
"name": "商人",
"type": "business",
"unlockConditions": { "type": "careerLevel", "op": ">=", "careerId": "apprentice", "value": 100 },
"dailyIncome": 5,
"expCurve": { "base": 80, "factor": 1.22 }
},
{
"id": "big_merchant",
"name": "大商人",
"type": "business",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "merchant", "value": 500 },
{ "type": "stat", "op": ">=", "stat": "business", "value": 30 }
]
},
"dailyIncome": 12,
"expCurve": { "base": 250, "factor": 1.26 }
},
{
"id": "wealthy_merchant",
"name": "富商",
"type": "business",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "big_merchant", "value": 1000 },
{ "type": "stat", "op": ">=", "stat": "business", "value": 60 }
]
},
"dailyIncome": 25,
"expCurve": { "base": 600, "factor": 1.3 }
},
{
"id": "tycoon",
"name": "巨贾",
"type": "business",
"unlockConditions": {
"type": "and",
"conditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "wealthy_merchant", "value": 2000 },
{ "type": "age", "op": ">=", "value": 40 },
{ "type": "stat", "op": ">=", "stat": "business", "value": 100 }
]
},
"dailyIncome": 50,
"expCurve": { "base": 1500, "factor": 1.35 }
} }
] ]
} }
+287 -123
View File
@@ -1,73 +1,257 @@
{ {
"events": [ "events": [
{
"id": "infant_cry",
"name": "啼哭",
"description": "你大声啼哭,引起了周围人的注意。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 20,
"minAge": 0,
"maxAge": 2,
"effects": [{ "type": "addStat", "stat": "body", "value": 0.5 }]
},
{
"id": "infant_sleep",
"name": "酣睡",
"description": "你睡了一个好觉,精神饱满。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 20,
"minAge": 0,
"maxAge": 2,
"effects": [{ "type": "addStat", "stat": "destiny", "value": 0.5 }]
},
{
"id": "toddler_walk",
"name": "学步",
"description": "你摇摇晃晃地迈出了人生的第一步。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 15,
"minAge": 1,
"maxAge": 4,
"effects": [{ "type": "addStat", "stat": "body", "value": 1 }]
},
{
"id": "toddler_words",
"name": "学语",
"description": "你咿咿呀呀地学着大人说话。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 15,
"minAge": 1,
"maxAge": 4,
"effects": [{ "type": "addStat", "stat": "wisdom", "value": 1 }]
},
{
"id": "child_play",
"name": "嬉戏",
"description": "你和邻家孩子在院子里玩耍,不亦乐乎。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 15,
"minAge": 3,
"maxAge": 8,
"effects": [{ "type": "addStat", "stat": "charm", "value": 1 }]
},
{
"id": "child_study",
"name": "启蒙",
"description": "先生在私塾教你认字读书,你颇有感悟。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 15,
"minAge": 5,
"maxAge": 12,
"effects": [{ "type": "addStat", "stat": "wisdom", "value": 1 }]
},
{ {
"id": "daily_training", "id": "daily_training",
"name": "日常修炼", "name": "日常修炼",
"description": "你坚持日常修炼,体质有所提升。", "description": "你坚持日常修炼,体质有所提升。",
"trigger": { "trigger": { "type": "random", "pool": "normal" },
"type": "random", "weight": 20,
"pool": "normal" "minAge": 6,
}, "effects": [{ "type": "addStat", "stat": "body", "value": 1 }]
"weight": 50,
"effects": [
{
"type": "addStat",
"stat": "体质",
"value": 1
}
]
}, },
{ {
"id": "study_hard", "id": "study_hard",
"name": "刻苦学习", "name": "刻苦学习",
"description": "你发奋读书,悟性有所提升。", "description": "你发奋读书,悟性有所提升。",
"trigger": { "trigger": { "type": "random", "pool": "normal" },
"type": "random", "weight": 20,
"pool": "normal" "minAge": 6,
}, "effects": [{ "type": "addStat", "stat": "wisdom", "value": 1 }]
"weight": 50, },
{
"id": "socialize",
"name": "广结善缘",
"description": "你与人为善,魅力有所提升。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 15,
"minAge": 8,
"effects": [{ "type": "addStat", "stat": "charm", "value": 1 }]
},
{
"id": "business_insight",
"name": "经商感悟",
"description": "你悟出了一些经商之道。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 10,
"minAge": 12,
"effects": [{ "type": "addStat", "stat": "business", "value": 1 }]
},
{
"id": "strategic_thinking",
"name": "谋略精进",
"description": "你思考国家大事,智谋有所提升。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 10,
"minAge": 14,
"effects": [{ "type": "addStat", "stat": "intelligence", "value": 1 }]
},
{
"id": "lucky_encounter",
"name": "奇遇",
"description": "你遇到了一位神秘老者,传授了你一些心法。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 5,
"minAge": 6,
"effects": [ "effects": [
{ "type": "addStat", "stat": "destiny", "value": 2 },
{ "type": "addStat", "stat": "wisdom", "value": 1 }
]
},
{
"id": "mountain_play",
"name": "山中玩耍",
"description": "你和伙伴去后山玩耍,发现一条隐蔽的小路。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 8,
"minAge": 6,
"isChoice": true,
"choices": [
{ {
"type": "addStat", "id": "explore",
"stat": "悟性", "text": "大胆进去看看",
"value": 1 "requirements": { "type": "stat", "op": ">=", "stat": "wisdom", "value": 5 },
"effects": [
{ "type": "addLog", "text": "你发现了一处灵气充沛的山洞!悟性+3" },
{ "type": "addStat", "stat": "wisdom", "value": 3 }
]
},
{
"id": "stay",
"text": "就在附近玩",
"effects": [{ "type": "addLog", "text": "你安全地度过了这一天。" }]
} }
] ]
}, },
{
"id": "street_beggar",
"name": "街头见闻",
"description": "你在街上看到一个衣衫褴褛的老乞丐在演示奇怪的招式。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 6,
"minAge": 8,
"isChoice": true,
"choices": [
{
"id": "watch",
"text": "认真观看",
"requirements": { "type": "stat", "op": ">=", "stat": "body", "value": 5 },
"effects": [
{ "type": "addLog", "text": "老乞丐看你骨骼惊奇,传你一招半式。体质+3" },
{ "type": "addStat", "stat": "body", "value": 3 }
]
},
{
"id": "ignore",
"text": "走开不理",
"effects": [{ "type": "addLog", "text": "你转身离开。" }]
}
]
},
{
"id": "taoist_encounter",
"name": "道观遇仙",
"description": "你在山上偶遇一位道士,他似乎对你很感兴趣。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 4,
"minAge": 10,
"isChoice": true,
"choices": [
{
"id": "ask_teach",
"text": "请求拜师",
"requirements": { "type": "stat", "op": ">=", "stat": "destiny", "value": 10 },
"effects": [
{ "type": "addLog", "text": "道士收你为记名弟子,赠你道碟。" },
{ "type": "setFlag", "flag": "met_taoist_priest", "value": true },
{ "type": "addItem", "itemId": "taoist_license" }
]
},
{
"id": "polite",
"text": "礼貌告辞",
"effects": [{ "type": "addLog", "text": "你礼貌地告辞了。" }]
}
]
},
{
"id": "treasure_hunt",
"name": "寻宝探险",
"description": "你听说附近有一处古迹,可能有宝物。",
"trigger": { "type": "random", "pool": "normal" },
"weight": 4,
"minAge": 15,
"isChoice": true,
"choices": [
{
"id": "go",
"text": "前往探险",
"requirements": { "type": "stat", "op": ">=", "stat": "body", "value": 15 },
"effects": [
{ "type": "addLog", "text": "你成功找到了一处宝藏,获得银两+500!" },
{ "type": "addMoney", "value": 500 }
]
},
{
"id": "skip",
"text": "太危险了,不去",
"effects": [{ "type": "addLog", "text": "你放弃了这次探险。" }]
}
]
},
{ {
"id": "invasion_military_40", "id": "invasion_military_40",
"name": "蛮族入侵", "name": "蛮族入侵",
"description": "北方蛮族大举入侵,边境告急!", "description": "北方蛮族大举入侵,边境告急!这是对你军事实力的第一次重大考验。",
"trigger": { "trigger": { "type": "age", "value": 40 },
"type": "age",
"value": 40
},
"isChoice": true, "isChoice": true,
"choices": [ "choices": [
{ {
"id": "resist", "id": "resist",
"text": "率军抵抗", "text": "率军抵抗(需要将军Lv.1000+",
"requirements": { "requirements": { "type": "careerLevel", "op": ">=", "careerId": "general", "value": 1000 },
"type": "careerLevel",
"careerId": "soldier",
"op": ">=",
"value": 50
},
"effects": [ "effects": [
{ { "type": "addLog", "text": "你率领大军击退了蛮族入侵,威震四方!军事危机解除。" },
"type": "addLog", { "type": "setFlag", "flag": "resisted_military_invasion", "value": true },
"text": "你率领大军击退了蛮族入侵,威震四方!" { "type": "addStat", "stat": "body", "value": 10 }
} ]
},
{
"id": "resist_weak",
"text": "勉强抵抗(需要千夫长Lv.500+",
"requirements": { "type": "careerLevel", "op": ">=", "careerId": "commander", "value": 500 },
"effects": [
{ "type": "addLog", "text": "你勉强守住了城池,但损失惨重。" },
{ "type": "addStat", "stat": "body", "value": 3 },
{ "type": "addStat", "stat": "destiny", "value": -5 }
] ]
}, },
{ {
"id": "flee", "id": "flee",
"text": "弃城逃跑", "text": "弃城逃跑",
"effects": [ "effects": [
{ { "type": "addLog", "text": "你选择了弃城逃跑,虽然保住了性命,但名声扫地。天命-10" },
"type": "addLog", { "type": "addStat", "stat": "destiny", "value": -10 }
"text": "你选择了弃城逃跑,虽然保住了性命,但名声扫地。"
}
] ]
} }
] ]
@@ -75,37 +259,36 @@
{ {
"id": "invasion_spiritual_45", "id": "invasion_spiritual_45",
"name": "天魔降世", "name": "天魔降世",
"description": "天魔降世,人间陷入混乱,你该如何应对?", "description": "天魔降世,人间陷入混乱,你该如何应对?这是对你修仙境界的重大考验。",
"trigger": { "trigger": { "type": "age", "value": 45 },
"type": "age",
"value": 45
},
"isChoice": true, "isChoice": true,
"choices": [ "choices": [
{ {
"id": "suppress", "id": "suppress",
"text": "出手镇压", "text": "出手镇压(需要天仙Lv.1000+",
"requirements": { "requirements": { "type": "careerLevel", "op": ">=", "careerId": "tian_xian", "value": 1000 },
"type": "careerLevel",
"careerId": "taoist",
"op": ">=",
"value": 50
},
"effects": [ "effects": [
{ { "type": "addLog", "text": "你以无上道法镇压天魔,拯救了苍生!精神危机解除。" },
"type": "addLog", { "type": "setFlag", "flag": "resisted_spiritual_invasion", "value": true },
"text": "你以无上道法镇压天魔,拯救了苍生!" { "type": "addStat", "stat": "wisdom", "value": 10 }
} ]
},
{
"id": "suppress_weak",
"text": "勉强封印(需要地仙Lv.500+",
"requirements": { "type": "careerLevel", "op": ">=", "careerId": "di_xian", "value": 500 },
"effects": [
{ "type": "addLog", "text": "你勉强封印了天魔,但自身也受到反噬。" },
{ "type": "addStat", "stat": "wisdom", "value": 3 },
{ "type": "addStat", "stat": "destiny", "value": -5 }
] ]
}, },
{ {
"id": "hide", "id": "hide",
"text": "躲藏起来", "text": "躲藏起来",
"effects": [ "effects": [
{ { "type": "addLog", "text": "你选择了躲藏起来,避开了这场浩劫。天命-10" },
"type": "addLog", { "type": "addStat", "stat": "destiny", "value": -10 }
"text": "你选择了躲藏起来,避开了这场浩劫。"
}
] ]
} }
] ]
@@ -113,37 +296,36 @@
{ {
"id": "invasion_political_50", "id": "invasion_political_50",
"name": "王朝崩坏", "name": "王朝崩坏",
"description": "王朝内部腐败,民不聊生,天下大乱。", "description": "王朝内部腐败,民不聊生,天下大乱。这是对你政治智慧的重大考验。",
"trigger": { "trigger": { "type": "age", "value": 50 },
"type": "age",
"value": 50
},
"isChoice": true, "isChoice": true,
"choices": [ "choices": [
{ {
"id": "assist", "id": "assist",
"text": "辅佐明君", "text": "辅佐明君(需要宰相Lv.1000+",
"requirements": { "requirements": { "type": "careerLevel", "op": ">=", "careerId": "chancellor", "value": 1000 },
"type": "stat",
"stat": "智力",
"op": ">=",
"value": 50
},
"effects": [ "effects": [
{ { "type": "addLog", "text": "你以宰相之智辅佐明君,力挽狂澜,拯救了王朝!政治危机解除。" },
"type": "addLog", { "type": "setFlag", "flag": "resisted_political_invasion", "value": true },
"text": "你以智谋辅佐明君,力挽狂澜,拯救了王朝。" { "type": "addStat", "stat": "intelligence", "value": 10 }
} ]
},
{
"id": "assist_weak",
"text": "尽力周旋(需要尚书Lv.500+",
"requirements": { "type": "careerLevel", "op": ">=", "careerId": "shangshu", "value": 500 },
"effects": [
{ "type": "addLog", "text": "你尽力周旋,保住了自己的一席之地。" },
{ "type": "addStat", "stat": "intelligence", "value": 3 },
{ "type": "addStat", "stat": "destiny", "value": -5 }
] ]
}, },
{ {
"id": "selfish", "id": "selfish",
"text": "独善其身", "text": "独善其身",
"effects": [ "effects": [
{ { "type": "addLog", "text": "你选择了独善其身,在乱世中保全了自己。天命-10" },
"type": "addLog", { "type": "addStat", "stat": "destiny", "value": -10 }
"text": "你选择了独善其身,在乱世中保全了自己。"
}
] ]
} }
] ]
@@ -151,69 +333,51 @@
{ {
"id": "final_crisis_60", "id": "final_crisis_60",
"name": "最终危机", "name": "最终危机",
"description": "世界面临最终危机,这是你最后的选择。", "description": "世界面临最终危机,这是你最后的选择。若实力足够,便可破局!",
"trigger": { "trigger": { "type": "age", "value": 60 },
"type": "age",
"value": 60
},
"isChoice": true, "isChoice": true,
"choices": [ "choices": [
{ {
"id": "general", "id": "general_breakthrough",
"text": "以将军之力平定天下", "text": "以元帅之力平定天下【将军破局】",
"requirements": { "requirements": { "type": "careerLevel", "op": ">=", "careerId": "marshal", "value": 5000 },
"type": "careerLevel",
"careerId": "soldier",
"op": ">=",
"value": 50
},
"effects": [ "effects": [
{ { "type": "addLog", "text": "你以元帅之力平定天下,成为一代名将!将军破局成功!" },
"type": "addLog", { "type": "setFlag", "flag": "breakthrough_general", "value": true },
"text": "你以将军之力平定天下,成为一代名将!" { "type": "addStat", "stat": "body", "value": 50 }
}
] ]
}, },
{ {
"id": "chancellor", "id": "chancellor_breakthrough",
"text": "以宰相之智治理国家", "text": "以宰相之智治理国家【宰相破局】",
"requirements": { "requirements": { "type": "careerLevel", "op": ">=", "careerId": "chancellor", "value": 5000 },
"type": "stat",
"stat": "智力",
"op": ">=",
"value": 50
},
"effects": [ "effects": [
{ { "type": "addLog", "text": "你以宰相之智治理国家,名垂青史!宰相破局成功!" },
"type": "addLog", { "type": "setFlag", "flag": "breakthrough_chancellor", "value": true },
"text": "你以宰相之智治理国家,名垂青史!" { "type": "addStat", "stat": "intelligence", "value": 50 }
}
] ]
}, },
{ {
"id": "cultivator", "id": "immortal_breakthrough",
"text": "以仙之道超脱轮回", "text": "以仙之道超脱轮回【修仙破局】",
"requirements": { "requirements": {
"type": "careerLevel", "type": "and",
"careerId": "taoist", "conditions": [
"op": ">=", { "type": "careerLevel", "op": ">=", "careerId": "tian_xian", "value": 5000 },
"value": 50 { "type": "hasArtifact", "artifactId": "immortal_sword", "level": 10 }
]
}, },
"effects": [ "effects": [
{ { "type": "addLog", "text": "你以修仙之道超脱轮回,飞升成仙!修仙破局成功!" },
"type": "addLog", { "type": "setFlag", "flag": "breakthrough_immortal", "value": true },
"text": "你以修仙之道超脱轮回,飞升成仙!" { "type": "addStat", "stat": "wisdom", "value": 50 }
}
] ]
}, },
{ {
"id": "powerless", "id": "powerless",
"text": "无力回天", "text": "无力回天",
"effects": [ "effects": [
{ { "type": "addLog", "text": "你感到无力回天,只能接受命运的安排。" }
"type": "addLog",
"text": "你感到无力回天,只能接受命运的安排。"
}
] ]
} }
] ]
+38 -12
View File
@@ -3,37 +3,63 @@
{ {
"id": "taoist_orphan", "id": "taoist_orphan",
"name": "道观弃婴", "name": "道观弃婴",
"description": "被遗弃在道观门口的婴儿,由道士抚养长大。开局自带道碟。", "description": "被道观收养长大,天生与道有缘",
"startingStats": { "body": 3, "wisdom": 5, "charm": 2, "destiny": 4, "business": 0, "intelligence": 3 }, "startingStats": { "body": 3, "wisdom": 5, "charm": 2, "destiny": 5, "business": 0, "intelligence": 2 },
"startingItems": ["taoist_license"], "startingItems": ["taoist_license"],
"unlockConditions": [] "unlockConditions": []
}, },
{ {
"id": "peasant", "id": "peasant",
"name": "农家", "name": "贫苦农家",
"description": "普通农户家的孩子。", "description": "出身贫寒,从小吃苦耐劳",
"startingStats": { "body": 4, "wisdom": 3, "charm": 3, "destiny": 3, "business": 1, "intelligence": 2 }, "startingStats": { "body": 5, "wisdom": 2, "charm": 2, "destiny": 2, "business": 1, "intelligence": 1 },
"startingItems": [], "startingItems": [],
"unlockConditions": [] "unlockConditions": []
}, },
{ {
"id": "merchant_son", "id": "merchant_son",
"name": "商贾之子", "name": "商贾之子",
"description": "商人家庭出身,从小耳濡目染经商之道", "description": "从小耳濡目染经商之道",
"startingStats": { "body": 2, "wisdom": 4, "charm": 4, "destiny": 3, "business": 5, "intelligence": 4 }, "startingStats": { "body": 2, "wisdom": 3, "charm": 4, "destiny": 3, "business": 5, "intelligence": 3 },
"startingItems": [],
"unlockConditions": []
},
{
"id": "noble",
"name": "世家子弟",
"description": "出身名门,资源丰富",
"startingStats": { "body": 3, "wisdom": 4, "charm": 5, "destiny": 4, "business": 2, "intelligence": 4 },
"startingItems": [],
"unlockConditions": []
},
{
"id": "military_family",
"name": "将门之后",
"description": "祖上为将,血脉中流淌着战斗的本能",
"startingStats": { "body": 6, "wisdom": 2, "charm": 3, "destiny": 3, "business": 0, "intelligence": 2 },
"startingItems": [], "startingItems": [],
"unlockConditions": [ "unlockConditions": [
{ "type": "reincarnation", "op": ">=", "value": 1 } { "type": "reincarnation", "op": ">=", "value": 1 }
] ]
}, },
{ {
"id": "noble", "id": "scholar_family",
"name": "世家子弟", "name": "书香门第",
"description": "出身名门望族,从小接受良好教育。", "description": "世代读书,家学渊源",
"startingStats": { "body": 3, "wisdom": 5, "charm": 5, "destiny": 5, "business": 3, "intelligence": 5 }, "startingStats": { "body": 1, "wisdom": 6, "charm": 3, "destiny": 3, "business": 1, "intelligence": 4 },
"startingItems": [], "startingItems": [],
"unlockConditions": [ "unlockConditions": [
{ "type": "reincarnation", "op": ">=", "value": 2 } { "type": "reincarnation", "op": ">=", "value": 1 }
]
},
{
"id": "reincarnator",
"name": "轮回者",
"description": "已历经多次轮回,带有前世的记忆碎片",
"startingStats": { "body": 4, "wisdom": 4, "charm": 4, "destiny": 6, "business": 3, "intelligence": 4 },
"startingItems": [],
"unlockConditions": [
{ "type": "reincarnation", "op": ">=", "value": 3 }
] ]
} }
] ]
+190 -5
View File
@@ -24,8 +24,81 @@
"tab": "item", "tab": "item",
"cost": 50, "cost": 50,
"unlockConditions": [], "unlockConditions": [],
"effects": [{ "type": "addStat", "stat": "body", "value": 5 }],
"resetOnRebirth": true
},
{
"id": "wisdom_pill",
"name": "悟道丹",
"tab": "item",
"cost": 50,
"unlockConditions": [],
"effects": [{ "type": "addStat", "stat": "wisdom", "value": 5 }],
"resetOnRebirth": true
},
{
"id": "charm_pill",
"name": "美颜丹",
"tab": "item",
"cost": 50,
"unlockConditions": [],
"effects": [{ "type": "addStat", "stat": "charm", "value": 5 }],
"resetOnRebirth": true
},
{
"id": "destiny_pill",
"name": "转运丹",
"tab": "item",
"cost": 100,
"unlockConditions": [{ "type": "age", "op": ">=", "value": 15 }],
"effects": [{ "type": "addStat", "stat": "destiny", "value": 5 }],
"resetOnRebirth": true
},
{
"id": "business_manual",
"name": "经商手册",
"tab": "item",
"cost": 80,
"unlockConditions": [],
"effects": [{ "type": "addStat", "stat": "business", "value": 5 }],
"resetOnRebirth": true
},
{
"id": "strategy_book",
"name": "谋略兵法",
"tab": "item",
"cost": 120,
"unlockConditions": [{ "type": "age", "op": ">=", "value": 15 }],
"effects": [{ "type": "addStat", "stat": "intelligence", "value": 5 }],
"resetOnRebirth": true
},
{
"id": "elixir_youth",
"name": "驻颜丹",
"tab": "item",
"cost": 500,
"unlockConditions": [
{ "type": "age", "op": ">=", "value": 30 },
{ "type": "careerLevel", "op": ">=", "careerId": "taoist_priest", "value": 100 }
],
"effects": [ "effects": [
{ "type": "addStat", "stat": "body", "value": 5 } { "type": "addLog", "text": "服用驻颜丹,寿命延长!" },
{ "type": "addStat", "stat": "body", "value": 10 }
],
"resetOnRebirth": true
},
{
"id": "mystic_scroll",
"name": "神秘卷轴",
"tab": "item",
"cost": 1000,
"unlockConditions": [
{ "type": "age", "op": ">=", "value": 25 },
{ "type": "stat", "op": ">=", "stat": "destiny", "value": 20 }
],
"effects": [
{ "type": "addStat", "stat": "wisdom", "value": 10 },
{ "type": "addStat", "stat": "destiny", "value": 5 }
], ],
"resetOnRebirth": true "resetOnRebirth": true
} }
@@ -37,8 +110,57 @@
"tab": "buff", "tab": "buff",
"cost": 200, "cost": 200,
"unlockConditions": [], "unlockConditions": [],
"effects": [{ "type": "addStat", "stat": "wisdom", "value": 10 }],
"resetOnRebirth": true
},
{
"id": "iron_body_buff",
"name": "铜皮铁骨",
"tab": "buff",
"cost": 200,
"unlockConditions": [],
"effects": [{ "type": "addStat", "stat": "body", "value": 10 }],
"resetOnRebirth": true
},
{
"id": "golden_tongue",
"name": "金口玉言",
"tab": "buff",
"cost": 250,
"unlockConditions": [{ "type": "age", "op": ">=", "value": 15 }],
"effects": [{ "type": "addStat", "stat": "charm", "value": 10 }],
"resetOnRebirth": true
},
{
"id": "merchant_aura",
"name": "财神附体",
"tab": "buff",
"cost": 300,
"unlockConditions": [{ "type": "stat", "op": ">=", "stat": "business", "value": 20 }],
"effects": [{ "type": "addStat", "stat": "business", "value": 15 }],
"resetOnRebirth": true
},
{
"id": "strategist_mind",
"name": "谋主之心",
"tab": "buff",
"cost": 350,
"unlockConditions": [{ "type": "stat", "op": ">=", "stat": "intelligence", "value": 20 }],
"effects": [{ "type": "addStat", "stat": "intelligence", "value": 15 }],
"resetOnRebirth": true
},
{
"id": "heaven_blessing",
"name": "天赐福缘",
"tab": "buff",
"cost": 500,
"unlockConditions": [
{ "type": "age", "op": ">=", "value": 20 },
{ "type": "stat", "op": ">=", "stat": "destiny", "value": 30 }
],
"effects": [ "effects": [
{ "type": "addStat", "stat": "wisdom", "value": 10 } { "type": "addStat", "stat": "destiny", "value": 15 },
{ "type": "addStat", "stat": "wisdom", "value": 5 }
], ],
"resetOnRebirth": true "resetOnRebirth": true
} }
@@ -51,11 +173,74 @@
"baseCost": 1000, "baseCost": 1000,
"costGrowth": 1.5, "costGrowth": 1.5,
"unlockConditions": [ "unlockConditions": [
{ "type": "careerLevel", "careerId": "taoist_priest", "op": ">=", "value": 50 } { "type": "careerLevel", "op": ">=", "careerId": "taoist_priest", "value": 50 }
], ],
"effects": [ "effects": [{ "type": "addStat", "stat": "body", "value": 10 }],
{ "type": "addStat", "stat": "body", "value": 10 } "resetOnRebirth": false,
"maxLevel": 10
},
{
"id": "jade_seal",
"name": "玉玺",
"tab": "artifact",
"baseCost": 2000,
"costGrowth": 1.6,
"unlockConditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "shangshu", "value": 100 }
], ],
"effects": [{ "type": "addStat", "stat": "intelligence", "value": 15 }],
"resetOnRebirth": false,
"maxLevel": 10
},
{
"id": "tiger_tally",
"name": "虎符",
"tab": "artifact",
"baseCost": 2000,
"costGrowth": 1.6,
"unlockConditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "general", "value": 100 }
],
"effects": [{ "type": "addStat", "stat": "body", "value": 15 }],
"resetOnRebirth": false,
"maxLevel": 10
},
{
"id": "divine_brush",
"name": "神笔",
"tab": "artifact",
"baseCost": 1500,
"costGrowth": 1.55,
"unlockConditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "hanlin", "value": 100 }
],
"effects": [{ "type": "addStat", "stat": "wisdom", "value": 12 }],
"resetOnRebirth": false,
"maxLevel": 10
},
{
"id": "treasure_bowl",
"name": "聚宝盆",
"tab": "artifact",
"baseCost": 3000,
"costGrowth": 1.7,
"unlockConditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "tycoon", "value": 100 }
],
"effects": [{ "type": "addStat", "stat": "business", "value": 20 }],
"resetOnRebirth": false,
"maxLevel": 10
},
{
"id": "talisman",
"name": "护身符",
"tab": "artifact",
"baseCost": 800,
"costGrowth": 1.45,
"unlockConditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "taoist_priest", "value": 30 }
],
"effects": [{ "type": "addStat", "stat": "destiny", "value": 8 }],
"resetOnRebirth": false, "resetOnRebirth": false,
"maxLevel": 10 "maxLevel": 10
} }
+86 -22
View File
@@ -4,61 +4,125 @@
"id": "sword_bone", "id": "sword_bone",
"name": "剑骨", "name": "剑骨",
"type": "talent", "type": "talent",
"description": "天生剑骨,军事职业经验+20%", "description": "天生适合练剑,江湖职业经验获取+30%",
"effects": [
{ "type": "addStat", "stat": "body", "value": 5 }
],
"unlockConditions": [ "unlockConditions": [
{ "type": "careerLevel", "careerId": "soldier", "op": ">=", "value": 100 } { "type": "careerLevel", "op": ">=", "careerId": "swordsman", "value": 100 }
],
"effects": [
{ "type": "addStat", "stat": "body", "value": 3 },
{ "type": "addStat", "stat": "charm", "value": 1 }
] ]
}, },
{ {
"id": "spirit_root", "id": "spirit_root",
"name": "灵根", "name": "灵根",
"type": "talent", "type": "talent",
"description": "身怀灵根,修仙职业经验+20%", "description": "对灵气有天然的亲和力,修仙职业经验获取+30%",
"effects": [
{ "type": "addStat", "stat": "wisdom", "value": 5 }
],
"unlockConditions": [ "unlockConditions": [
{ "type": "careerLevel", "careerId": "taoist_priest", "op": ">=", "value": 100 } { "type": "careerLevel", "op": ">=", "careerId": "zhen_ren", "value": 100 }
],
"effects": [
{ "type": "addStat", "stat": "wisdom", "value": 4 },
{ "type": "addStat", "stat": "destiny", "value": 2 }
] ]
}, },
{ {
"id": "eidetic_memory", "id": "eidetic_memory",
"name": "过目不忘", "name": "过目不忘",
"type": "talent", "type": "talent",
"description": "记忆力超群,书生职业经验+20%", "description": "看过的知识不会忘记,文官职业经验获取+30%",
"unlockConditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "xiucai", "value": 100 }
],
"effects": [ "effects": [
{ "type": "addStat", "stat": "wisdom", "value": 3 }, { "type": "addStat", "stat": "wisdom", "value": 3 },
{ "type": "addStat", "stat": "intelligence", "value": 3 } { "type": "addStat", "stat": "intelligence", "value": 2 }
], ]
},
{
"id": "iron_body",
"name": "铁骨",
"type": "talent",
"description": "身体强健,不易受伤",
"unlockConditions": [ "unlockConditions": [
{ "type": "careerLevel", "careerId": "student", "op": ">=", "value": 200 } { "type": "stat", "op": ">=", "stat": "body", "value": 30 }
],
"effects": [
{ "type": "addStat", "stat": "body", "value": 5 }
]
},
{
"id": "charming",
"name": "天生丽质",
"type": "talent",
"description": "容貌出众,容易获得好感",
"unlockConditions": [
{ "type": "stat", "op": ">=", "stat": "charm", "value": 30 }
],
"effects": [
{ "type": "addStat", "stat": "charm", "value": 5 }
] ]
}, },
{ {
"id": "lucky_star", "id": "lucky_star",
"name": "福星高照", "name": "福星高照",
"type": "talent", "type": "talent",
"description": "天生好运,事件收益+20%", "description": "运气比别人好一些",
"unlockConditions": [
{ "type": "stat", "op": ">=", "stat": "destiny", "value": 30 }
],
"effects": [ "effects": [
{ "type": "addStat", "stat": "destiny", "value": 5 } { "type": "addStat", "stat": "destiny", "value": 5 }
],
"unlockConditions": [
{ "type": "stat", "stat": "destiny", "op": ">=", "value": 30 }
] ]
}, },
{ {
"id": "business_genius", "id": "business_genius",
"name": "商奇才", "name": "商奇才",
"type": "talent", "type": "talent",
"description": "商业嗅觉敏锐,银两收入+20%", "description": "对商机有敏锐的嗅觉,经商职业经验获取+30%",
"unlockConditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "big_merchant", "value": 100 }
],
"effects": [ "effects": [
{ "type": "addStat", "stat": "business", "value": 5 } { "type": "addStat", "stat": "business", "value": 5 }
], ]
},
{
"id": "strategist",
"name": "谋圣之资",
"type": "talent",
"description": "天生擅长谋略,政治职业经验获取+30%",
"unlockConditions": [ "unlockConditions": [
{ "type": "stat", "stat": "business", "op": ">=", "value": 30 } { "type": "careerLevel", "op": ">=", "careerId": "shilang", "value": 100 }
],
"effects": [
{ "type": "addStat", "stat": "intelligence", "value": 5 }
]
},
{
"id": "war_blood",
"name": "战魂血脉",
"type": "talent",
"description": "祖上为将,血脉中流淌着战斗的本能,武将职业经验获取+30%",
"unlockConditions": [
{ "type": "careerLevel", "op": ">=", "careerId": "general", "value": 100 }
],
"effects": [
{ "type": "addStat", "stat": "body", "value": 3 },
{ "type": "addStat", "stat": "intelligence", "value": 2 }
]
},
{
"id": "reincarnation_insight",
"name": "轮回感悟",
"type": "talent",
"description": "经历多次轮回后获得的对天道的感悟",
"unlockConditions": [
{ "type": "reincarnation", "op": ">=", "value": 5 }
],
"effects": [
{ "type": "addStat", "stat": "wisdom", "value": 5 },
{ "type": "addStat", "stat": "destiny", "value": 5 }
] ]
} }
] ]
+489
View File
@@ -0,0 +1,489 @@
// ==================== 事件定义 ====================
import { state, addLog } from '../engine/state.js';
import { gainExp, setActiveCareer, CAREERS } from '../engine/expSystem.js';
import { eventEngine } from '../engine/eventEngine.js';
// 工具函数
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
// ==================== 幼年事件 (age 0-6) ====================
const CHILD_EVENTS = [
{
id: 'mountain_play',
title: '山中玩耍',
text: '你和伙伴去后山玩耍,发现一条隐蔽的小路。',
condition: (s) => s.age >= 3 && s.age <= 6 && !s.worldFlags.mountain_path_seen,
weight: 10,
isChoice: true,
defaultChoice: 1,
choices: [
{
text: '大胆进去看看',
requirement: (s) => s.stats.wisdom >= 3,
effect: (s) => {
s.worldFlags.mountain_path_seen = true;
if (s.stats.wisdom >= 4 || s.memories.includes('cultivation_memory')) {
addLog('你发现了一处灵气充沛的山洞!');
gainExp('student', 30);
return { flags: { found_cave: true } };
}
addLog('你在山里迷了路,被猎人救回。');
gainExp('farmer', 15);
return {};
}
},
{
text: '就在附近玩',
effect: (s) => {
addLog('你安全地度过了这一天。');
return {};
}
}
]
},
{
id: 'street_beggar',
title: '街头见闻',
text: '你在街上看到一个衣衫褴褛的老乞丐在演示奇怪的招式。',
condition: (s) => s.age >= 4 && s.age <= 6,
weight: 8,
isChoice: true,
defaultChoice: 1,
choices: [
{
text: '认真观看',
requirement: (s) => s.stats.body >= 3,
effect: (s) => {
if (s.stats.body >= 4 || s.memories.includes('wuxia_memory')) {
addLog('老乞丐看你骨骼惊奇,传你一招半式。');
gainExp('hoodlum', 30);
return { flags: { learned_from_beggar: true } };
}
addLog('你看不太懂,但记住了招式的大概。');
return {};
}
},
{
text: '走开不理',
effect: (s) => {
addLog('你转身离开。');
return {};
}
}
]
},
{
id: 'family_reading',
title: '父亲读书',
text: '你的父亲在灯下读书,你好奇地凑过去。',
condition: (s) => s.age >= 3 && s.age <= 6 && s.family >= 0,
weight: 5,
isChoice: false,
impact: 0.2,
effect: (s) => {
addLog('父亲教你认了几个字。');
gainExp('student', 10);
return { flags: { father_taught: true } };
}
},
{
id: 'play_fight',
title: '街头打架',
text: '你在街头和别的孩子打了一架。',
condition: (s) => s.age >= 4 && s.age <= 6 && s.family <= 0,
weight: 6,
isChoice: false,
impact: 0.3,
effect: (s) => {
addLog('你打赢了!虽然鼻青脸肿,但感觉很痛快。');
gainExp('hoodlum', 15);
s.stats.body += 0.1;
return {};
}
},
];
// ==================== 少年事件 (age 7-15) ====================
const YOUTH_EVENTS = [
{
id: 'cultivation_test',
title: '宗门测试',
text: '你前往附近的修仙宗门参加资质测试。你的悟性将决定结果。',
condition: (s) => s.age >= 8 && s.age <= 12 && (s.worldFlags.found_cave || s.stats.wisdom >= 5),
weight: 8,
isChoice: true,
defaultChoice: 0,
choices: [
{
text: '参加测试',
effect: (s) => {
if (s.stats.wisdom + s.stats.luck >= 7) {
addLog('你被收为外门弟子!');
setActiveCareer('student');
gainExp('student', 50);
} else {
addLog('资质平庸,只能做杂役。');
setActiveCareer('farmer');
gainExp('farmer', 20);
}
return {};
}
},
{
text: '放弃修仙,另寻出路',
effect: (s) => {
setActiveCareer('hoodlum');
gainExp('hoodlum', 30);
return {};
}
}
]
},
{
id: 'wuxia_entrance',
title: '江湖入门',
text: '你决定闯荡江湖,面前有两条路。',
condition: (s) => s.age >= 8 && s.age <= 14 && (s.worldFlags.learned_from_beggar || s.stats.body >= 5),
weight: 8,
isChoice: true,
defaultChoice: 0,
choices: [
{
text: '加入镖局',
effect: (s) => {
addLog('你成为了一名镖师学徒。');
setActiveCareer('hoodlum');
gainExp('hoodlum', 50);
return {};
}
},
{
text: '独自流浪',
effect: (s) => {
addLog('你成为了一名流浪剑客。');
setActiveCareer('hoodlum');
gainExp('hoodlum', 40);
s.stats.body += 0.5;
return {};
}
}
]
},
{
id: 'smith_apprentice',
title: '铁匠铺',
text: '镇上的铁匠铺正在招学徒。',
condition: (s) => s.age >= 9 && s.age <= 14,
weight: 6,
isChoice: true,
defaultChoice: 0,
choices: [
{
text: '去当学徒',
effect: (s) => {
addLog('你开始学习锻造。');
setActiveCareer('apprentice');
gainExp('apprentice', 40);
return {};
}
},
{
text: '不感兴趣',
effect: (s) => {
addLog('你转身离开。');
return {};
}
}
]
},
{
id: 'school_days',
title: '学堂',
text: '你被送去学堂读书。',
condition: (s) => s.age >= 7 && s.age <= 12 && s.family >= 0,
weight: 7,
isChoice: false,
impact: 0.3,
effect: (s) => {
addLog('先生教你识字读书。');
gainExp('student', 20);
s.stats.wisdom += 0.2;
return {};
}
},
{
id: 'farm_work',
title: '田间劳作',
text: '你跟着父亲在田里干活。',
condition: (s) => s.age >= 7 && s.age <= 14 && s.family <= 0,
weight: 7,
isChoice: false,
impact: 0.2,
effect: (s) => {
addLog('你学会了种地。');
gainExp('farmer', 15);
s.stats.body += 0.2;
return {};
}
},
];
// ==================== 成年事件 (age 16+) ====================
const ADULT_EVENTS = [
{
id: 'breakthrough',
title: '修炼突破',
text: '你在修炼中遇到了瓶颈,需要做出选择。',
condition: (s) => s.age >= 16 && s.careers.student?.active && s.careers.student.level >= 5,
weight: 5,
isChoice: true,
defaultChoice: 1,
choices: [
{
text: '闭关苦修(高风险高回报)',
requirement: (s) => s.stats.wisdom >= 4,
effect: (s) => {
if (s.stats.wisdom >= 5 || Math.random() < 0.6) {
addLog('突破成功!修为大涨!');
gainExp('student', 100);
} else {
addLog('走火入魔,受了内伤。');
s.stats.body -= 1;
gainExp('student', 20);
}
return {};
}
},
{
text: '稳扎稳打',
effect: (s) => {
gainExp('student', 50);
return {};
}
}
]
},
{
id: 'gang_fight',
title: '帮派火并',
text: '你所在的帮派和敌对帮派发生了冲突。',
condition: (s) => s.age >= 16 && s.careers.hoodlum?.active && s.careers.hoodlum.level >= 5,
weight: 5,
isChoice: true,
defaultChoice: 0,
choices: [
{
text: '带头冲锋',
requirement: (s) => s.stats.body >= 5,
effect: (s) => {
if (s.stats.body + s.stats.luck >= 7) {
addLog('你大胜而归,名震一方!');
gainExp('hoodlum', 80);
s.stats.charm += 0.5;
} else {
addLog('你受了重伤,险些丧命。');
s.stats.body -= 2;
}
return {};
}
},
{
text: '暗中偷袭',
effect: (s) => {
addLog('你偷袭成功,获得了不少好处。');
gainExp('hoodlum', 50);
return {};
}
}
]
},
{
id: 'smith_masterpiece',
title: '锻造 masterpiece',
text: '你锻造出了一件让自己都惊讶的作品。',
condition: (s) => s.age >= 16 && s.careers.apprentice?.active && s.careers.apprentice.level >= 8,
weight: 4,
isChoice: false,
impact: 0.5,
effect: (s) => {
addLog('你的技艺得到了认可!');
gainExp('apprentice', 60);
s.stats.wisdom += 0.3;
return {};
}
},
{
id: 'harvest_festival',
title: '丰收节',
text: '村里举行丰收节,你参加了庆祝活动。',
condition: (s) => s.age >= 16 && s.careers.farmer?.active,
weight: 4,
isChoice: false,
impact: 0.2,
effect: (s) => {
addLog('丰收的喜悦让你充满干劲。');
gainExp('farmer', 30);
s.stats.charm += 0.2;
return {};
}
},
];
// ==================== 通用日常事件 ====================
const COMMON_EVENTS = [
// 普通文本事件
{
id: 'daily_morning',
title: '清晨',
text: '清晨的阳光照在脸上,你开始了新的一天。',
condition: () => true,
weight: 20,
isChoice: false,
impact: 0,
effect: () => ({})
},
{
id: 'daily_training',
title: '修炼',
text: '你像往常一样修炼了一会儿。',
condition: (s) => Object.values(s.careers).some(c => c.active),
weight: 15,
isChoice: false,
impact: 0.1,
effect: (s) => {
const activeCareer = Object.entries(s.careers).find(([_, c]) => c.active)?.[0];
if (activeCareer) {
gainExp(activeCareer, 5);
}
return {};
}
},
{
id: 'npc_zhangsan_marriage',
title: '婚事',
text: '张三今天娶了邻村的姑娘。',
condition: (s) => s.day >= 100 && !s.worldFlags.zhangsan_married,
weight: 3,
isChoice: false,
impact: 0.1,
effect: (s) => {
s.worldFlags.zhangsan_married = true;
return {};
}
},
{
id: 'weather_rain',
title: '下雨',
text: '今天下了一场大雨。',
condition: () => true,
weight: 10,
isChoice: false,
impact: 0,
effect: () => ({})
},
{
id: 'market_day',
title: '集市',
text: '今天是赶集的日子,街上热闹非凡。',
condition: (s) => s.day % 7 === 0,
weight: 8,
isChoice: false,
impact: 0.2,
effect: (s) => {
s.stats.charm += 0.1;
return {};
}
},
// 影响文本事件
{
id: 'impact_injury',
title: '意外受伤',
text: '你在训练中不小心受了轻伤。',
condition: (s) => s.age >= 10 && Math.random() < 0.3,
weight: 5,
isChoice: false,
impact: 0.4,
effect: (s) => {
s.stats.body -= 0.5;
addLog('训练时不小心受了伤,需要休养几天。');
return {};
}
},
{
id: 'impact_insight',
title: '顿悟',
text: '你在修炼中突然有所领悟。',
condition: (s) => s.age >= 15 && s.stats.wisdom >= 4,
weight: 4,
isChoice: false,
impact: 0.5,
effect: (s) => {
const activeCareer = Object.entries(s.careers).find(([_, c]) => c.active)?.[0];
if (activeCareer) {
gainExp(activeCareer, 30);
}
addLog('顿悟时刻!修为精进。');
return {};
}
},
{
id: 'impact_npc_gift',
title: '赠礼',
text: '一位老友来访,给你带来了一份礼物。',
condition: (s) => s.age >= 20,
weight: 3,
isChoice: false,
impact: 0.4,
effect: (s) => {
s.stats.luck += 0.3;
addLog('朋友送你一块奇石,据说能带来好运。');
return {};
}
},
{
id: 'impact_enemy_scout',
title: '敌情',
text: '边境传来消息,蛮族的斥候出现在附近。',
condition: (s) => s.enemyProgress.military >= 30,
weight: 6,
isChoice: false,
impact: 0.6,
effect: (s) => {
s.crisisLevel += 5;
addLog('蛮族越来越近,人心惶惶。');
return {};
}
},
];
// ==================== 注册所有事件 ====================
export function registerAllEvents() {
for (const ev of CHILD_EVENTS) {
eventEngine.register(ev);
}
for (const ev of YOUTH_EVENTS) {
eventEngine.register(ev);
}
for (const ev of ADULT_EVENTS) {
eventEngine.register(ev);
}
for (const ev of COMMON_EVENTS) {
eventEngine.register(ev);
}
}
// 导出事件定义(供其他模块使用)
export { CHILD_EVENTS, YOUTH_EVENTS, ADULT_EVENTS, COMMON_EVENTS };
+272
View File
@@ -0,0 +1,272 @@
// ==================== NPC系统 ====================
import { state, addLog } from '../engine/state.js';
// NPC定义
export const NPCS = {
old_beggar: {
id: 'old_beggar',
name: '老乞丐',
description: '一个衣衫褴褛的神秘老人',
relation: { trust: 0, favor: 0 },
// NPC事件线
eventLine: [
{
day: 30,
eventId: 'beggar_first_meet',
condition: (s) => !s.worldFlags.met_old_beggar,
},
{
day: 200,
eventId: 'beggar_teach',
condition: (s) => s.worldFlags.met_old_beggar && s.npcs.old_beggar?.favor > 20,
},
{
day: 500,
eventId: 'beggar_death',
condition: (s) => s.worldFlags.met_old_beggar && s.age > 50,
},
],
// 互动选项
interactions: [
{
id: 'give_food',
text: '给食物',
effect: (s) => {
s.npcs.old_beggar.favor += 10;
s.npcs.old_beggar.trust += 5;
addLog('老乞丐感激地收下了食物。');
}
},
{
id: 'ask_story',
text: '询问往事',
requirement: (s) => s.npcs.old_beggar.trust > 10,
effect: (s) => {
addLog('老乞丐讲述了一个关于江湖的传说...');
s.stats.wisdom += 0.2;
}
},
]
},
taoist_priest: {
id: 'taoist_priest',
name: '云游道士',
description: '一位行踪不定的修行者',
relation: { trust: 0, favor: 0 },
eventLine: [
{
day: 100,
eventId: 'taoist_first_meet',
condition: (s) => !s.worldFlags.met_taoist && s.stats.wisdom >= 3,
},
{
day: 300,
eventId: 'taoist_test',
condition: (s) => s.worldFlags.met_taoist && s.stats.wisdom >= 5,
},
],
interactions: [
{
id: 'ask_cultivation',
text: '请教修炼',
effect: (s) => {
addLog('道士传授了你一些呼吸吐纳之法。');
s.stats.wisdom += 0.3;
}
},
]
},
zhangsan: {
id: 'zhangsan',
name: '张三',
description: '村里的普通村民',
relation: { trust: 0, favor: 0 },
eventLine: [
{
day: 50,
eventId: 'zhangsan_marriage',
condition: (s) => !s.worldFlags.zhangsan_married,
},
{
day: 300,
eventId: 'zhangsan_child',
condition: (s) => s.worldFlags.zhangsan_married,
},
{
day: 800,
eventId: 'zhangsan_death',
condition: (s) => s.worldFlags.zhangsan_married && s.age > 60,
},
],
},
lisi: {
id: 'lisi',
name: '李四',
description: '镇上的商人',
relation: { trust: 0, favor: 0 },
eventLine: [
{
day: 150,
eventId: 'lisi_shop',
condition: (s) => !s.worldFlags.met_lisi,
},
],
interactions: [
{
id: 'trade',
text: '交易',
effect: (s) => {
addLog('和李四做了一笔生意。');
s.stats.charm += 0.1;
}
},
{
id: 'ask_news',
text: '打听消息',
effect: (s) => {
addLog('李四告诉你最近蛮族在边境活动频繁。');
s.stats.wisdom += 0.1;
}
},
]
},
wangwu: {
id: 'wangwu',
name: '王五',
description: '一个神秘的危险人物',
relation: { trust: -20, favor: -20 },
eventLine: [
{
day: 200,
eventId: 'wangwu_ambush',
condition: (s) => !s.worldFlags.met_wangwu && s.careers.hoodlum?.level >= 5,
},
{
day: 600,
eventId: 'wangwu_revenge',
condition: (s) => s.worldFlags.wangwu_humiliated,
},
],
},
};
// NPC事件定义
export const NPC_EVENTS = [
{
id: 'beggar_first_meet',
title: '偶遇',
text: '你在街头遇到了一个衣衫褴褛的老乞丐。',
isChoice: true,
defaultChoice: 0,
choices: [
{
text: '给些银钱',
effect: (s) => {
s.worldFlags.met_old_beggar = true;
s.npcs.old_beggar = { favor: 10, trust: 5 };
addLog('老乞丐感激地收下了。');
return {};
}
},
{
text: '视而不见',
effect: (s) => {
s.worldFlags.met_old_beggar = true;
s.npcs.old_beggar = { favor: -5, trust: 0 };
return {};
}
}
]
},
{
id: 'taoist_first_meet',
title: '云游道士',
text: '一位道士在山中与你相遇。',
isChoice: true,
defaultChoice: 0,
choices: [
{
text: '请教修行',
effect: (s) => {
s.worldFlags.met_taoist = true;
s.npcs.taoist_priest = { favor: 15, trust: 10 };
addLog('道士对你颇有好感,传授了一些心法。');
s.stats.wisdom += 0.5;
return {};
}
},
{
text: '礼貌离开',
effect: (s) => {
s.worldFlags.met_taoist = true;
s.npcs.taoist_priest = { favor: 0, trust: 0 };
return {};
}
}
]
},
{
id: 'zhangsan_marriage',
title: '喜事',
text: '张三今天娶了邻村的姑娘,村里举行了热闹的婚礼。',
isChoice: false,
effect: (s) => {
s.worldFlags.zhangsan_married = true;
addLog('张三结婚了!村里喜气洋洋。');
return {};
}
},
{
id: 'wangwu_ambush',
title: '埋伏',
text: '王五带着人堵住了你的去路。',
isChoice: true,
defaultChoice: 0,
choices: [
{
text: ' fight',
requirement: (s) => s.stats.body >= 5,
effect: (s) => {
if (s.stats.body + s.stats.luck >= 7) {
addLog('你打败了王五,他灰溜溜地逃走了。');
s.worldFlags.wangwu_humiliated = true;
} else {
addLog('你被王五打伤了。');
s.stats.body -= 2;
}
return {};
}
},
{
text: '逃跑',
effect: (s) => {
addLog('你趁乱逃走了。');
return {};
}
}
]
},
];
// 注册NPC事件
export function registerNPCEvents(eventEngine) {
for (const ev of NPC_EVENTS) {
eventEngine.register(ev);
}
}
// 检查NPC事件线
export function checkNPCEvents() {
for (const [npcId, npc] of Object.entries(NPCS)) {
for (const eventNode of npc.eventLine || []) {
if (eventNode.condition(state)) {
// 事件条件满足,由事件引擎决定是否触发
}
}
}
}
+219
View File
@@ -0,0 +1,219 @@
// ==================== 词条系统 ====================
// 词条类型:identity(身份) / talent(天赋) / item(携带物) / curse(诅咒)
export const ENTRIES = [
// === 身份词条 ===
{
id: 'orphan',
name: '孤儿',
type: 'identity',
desc: '无父无母,街头生存经验丰富',
effect: { target: 'hoodlum', expRate: 0.3, body: 1 },
unlockCondition: (history) => history.some(h => h.family < -1),
},
{
id: 'royal_blood',
name: '皇室后裔',
type: 'identity',
desc: '身上流着高贵的血液',
effect: { target: 'all', expRate: 0.1, charm: 2 },
unlockCondition: (history) => history.some(h => h.family >= 2 && h.age >= 50),
},
{
id: 'merchant_son',
name: '商贾之子',
type: 'identity',
desc: '从小耳濡目染经商之道',
effect: { target: 'all', expRate: 0.15, charm: 1 },
unlockCondition: (history) => history.some(h => h.family > 0),
},
{
id: 'peasant',
name: '贫苦出身',
type: 'identity',
desc: '从小吃苦,意志坚韧',
effect: { target: 'all', expRate: 0.1, body: 1, wisdom: 1 },
unlockCondition: () => true, // 默认解锁
},
{
id: 'noble',
name: '世家子弟',
type: 'identity',
desc: '出身名门,资源丰富',
effect: { target: 'all', expRate: 0.2, charm: 2, luck: 1 },
unlockCondition: (history) => history.some(h => h.family >= 2),
},
// === 天赋词条 ===
{
id: 'sword_bone',
name: '剑骨',
type: 'talent',
desc: '天生适合练剑',
effect: { target: 'wuxia', expRate: 0.5, body: 1 },
unlockCondition: (history) => history.some(h => h.careers.wuxia?.level >= 20),
},
{
id: 'spirit_root',
name: '灵根',
type: 'talent',
desc: '对灵气有天然的亲和力',
effect: { target: 'sword_immortal', expRate: 0.4, wisdom: 2 },
unlockCondition: (history) => history.some(h => h.careers.sword_immortal?.level >= 10),
},
{
id: 'eidetic_memory',
name: '过目不忘',
type: 'talent',
desc: '看过的知识不会忘记',
effect: { target: 'student', expRate: 0.4, wisdom: 2 },
unlockCondition: (history) => history.some(h => h.careers.student?.level >= 10),
},
{
id: 'iron_body',
name: '铁骨',
type: 'talent',
desc: '身体强健,不易受伤',
effect: { target: 'all', expRate: 0.1, body: 3 },
unlockCondition: (history) => history.some(h => h.stats?.body >= 15),
},
{
id: 'charming',
name: '天生丽质',
type: 'talent',
desc: '容貌出众,容易获得好感',
effect: { target: 'all', expRate: 0.1, charm: 3 },
unlockCondition: (history) => history.some(h => h.stats?.charm >= 15),
},
{
id: 'lucky_star',
name: '福星高照',
type: 'talent',
desc: '运气比别人好一些',
effect: { target: 'all', expRate: 0.15, luck: 3 },
unlockCondition: (history) => history.some(h => h.stats?.luck >= 15),
},
{
id: 'street_smart',
name: '街头智慧',
type: 'talent',
desc: '在底层社会如鱼得水',
effect: { target: 'hoodlum', expRate: 0.4, body: 1, luck: 1 },
unlockCondition: (history) => history.some(h => h.careers.hoodlum?.level >= 10),
},
{
id: 'smith_talent',
name: '工匠之魂',
type: 'talent',
desc: '对锻造有独特的天赋',
effect: { target: 'apprentice', expRate: 0.4, wisdom: 1 },
unlockCondition: (history) => history.some(h => h.careers.apprentice?.level >= 10),
},
// === 携带物词条 ===
{
id: 'jade_pendant',
name: '祖传玉佩',
type: 'item',
desc: '家族传承的玉佩,似乎蕴含着某种力量',
effect: { target: 'all', expRate: 0.1, luck: 2 },
unlockCondition: (history) => history.some(h => h.family > 0 && h.age >= 30),
},
{
id: 'mysterious_seed',
name: '神秘种子',
type: 'item',
desc: '一颗散发着微光的种子',
effect: { target: 'farmer', expRate: 0.3, wisdom: 1 },
unlockCondition: (history) => history.some(h => h.careers.farmer?.level >= 10),
},
{
id: 'old_sword',
name: '旧剑',
type: 'item',
desc: '一把生锈的剑,但握在手中 feels right',
effect: { target: 'wuxia', expRate: 0.2, body: 1 },
unlockCondition: (history) => history.some(h => h.careers.wuxia?.level >= 5),
},
{
id: 'ancient_book',
name: '古籍残卷',
type: 'item',
desc: '一本看不懂的古书',
effect: { target: 'student', expRate: 0.3, wisdom: 2 },
unlockCondition: (history) => history.some(h => h.careers.student?.level >= 10),
},
// === 诅咒词条 ===
{
id: 'short_lived',
name: '短命',
type: 'curse',
desc: '寿命比别人短',
effect: { target: 'all', expRate: 0.2, body: -2 },
unlockCondition: (history) => history.filter(h => h.age < 30).length >= 3,
},
{
id: 'disaster_magnet',
name: '招灾',
type: 'curse',
desc: '坏事总找上你',
effect: { target: 'all', expRate: 0.1, luck: -3 },
unlockCondition: (history) => history.filter(h => h.age < 20).length >= 5,
},
{
id: 'lone_star',
name: '孤星',
type: 'curse',
desc: '身边的人总是离你而去',
effect: { target: 'all', expRate: 0.15, charm: -2 },
unlockCondition: (history) => history.some(h => h.age >= 60 && !h.memories?.includes('married')),
},
{
id: 'poisoned',
name: '毒物警觉',
type: 'curse',
desc: '曾经被毒杀,对毒物异常敏感',
effect: { target: 'all', expRate: 0.1, wisdom: 1 },
unlockCondition: (history) => history.some(h => h.deathReason?.includes('毒')),
},
{
id: 'wounded',
name: '旧伤',
type: 'curse',
desc: '身上带着无法愈合的旧伤',
effect: { target: 'all', expRate: 0.1, body: -1 },
unlockCondition: (history) => history.some(h => h.deathReason?.includes('战')),
},
];
// 获取词条定义
export function getEntry(id) {
return ENTRIES.find(e => e.id === id);
}
// 获取所有已解锁的词条
export function getUnlockedEntries(state) {
return ENTRIES.filter(e => {
if (state.unlockedEntries.has(e.id)) return true;
if (e.unlockCondition && e.unlockCondition(state.history || [])) {
state.unlockedEntries.add(e.id);
return true;
}
return false;
});
}
// 检查是否有新词条解锁
export function checkNewEntries(state) {
const newEntries = [];
for (const entry of ENTRIES) {
if (state.unlockedEntries.has(entry.id)) continue;
if (entry.unlockCondition && entry.unlockCondition(state.history || [])) {
state.unlockedEntries.add(entry.id);
newEntries.push(entry);
}
}
return newEntries;
}
+173
View File
@@ -0,0 +1,173 @@
// ==================== 死亡与结算 ====================
import { state, addLog, resetStateForRebirth } from './state.js';
import { CAREERS } from './expSystem.js';
import { checkNewEntries as checkNewEntriesFromTalents } from '../content/talents.js';
// 检查是否死亡
export function checkDeath() {
if (!state.alive) return true;
// 年龄死亡
if (state.age >= 100) {
die('寿终正寝');
return true;
}
if (state.age >= 50) {
const deathChance = getAgeDeathChance(state.age);
if (Math.random() < deathChance) {
die('寿终正寝');
return true;
}
}
// 外敌死亡
for (const [type, progress] of Object.entries(state.enemyProgress)) {
if (progress >= 100 && Math.random() < 0.4) {
const names = { military: '死于蛮族入侵', spiritual: '被天魔吞噬', political: '王朝覆灭,死于战乱' };
die(names[type] || '死于外敌');
return true;
}
}
// 体质死亡
if (state.stats.body <= 0) {
die('体弱身亡');
return true;
}
// 危机感死亡(极端情况)
if (state.crisisLevel >= 100 && Math.random() < 0.3) {
die('在极度恐惧中精神崩溃');
return true;
}
return false;
}
function getAgeDeathChance(age) {
if (age < 50) return 0;
if (age < 60) return 0.02;
if (age < 70) return 0.08;
if (age < 80) return 0.20;
if (age < 90) return 0.45;
if (age < 100) return 0.80;
return 1.0;
}
// 死亡结算
function die(reason) {
if (!state.alive) return;
state.alive = false;
addLog(`你死了。原因:${reason}`, 'death');
// 结算元经验
const metaGains = {};
for (const [id, career] of Object.entries(state.careers)) {
const config = CAREERS[id];
if (!config) continue;
let totalExp = career.exp;
for (let i = 0; i < career.level; i++) {
totalExp += Math.floor(config.baseExp * Math.pow(config.expCurve, i));
}
const gain = Math.floor(totalExp * 0.1);
state.metaExp[id] = (state.metaExp[id] || 0) + gain;
metaGains[id] = gain;
}
// 解锁新词条
const newEntries = checkNewEntriesFromTalents(state);
for (const entry of newEntries) {
state.unlockedEntries.add(entry.id);
}
// 外敌进度跨轮继承(保留30%
for (const key of Object.keys(state.enemyProgress)) {
state.enemyProgress[key] = Math.min(100, state.enemyProgress[key] * 0.3 + 5);
}
// 触发死亡结算界面
if (typeof window !== 'undefined' && window.onDeath) {
window.onDeath({
reason,
age: state.age,
day: state.day,
careers: Object.fromEntries(
Object.entries(state.careers).map(([k, v]) => [k, { level: v.level, name: CAREERS[k]?.name }])
),
metaGains,
newEntries,
history: getHistoryComparison(),
});
}
}
// 历史对比
function getHistoryComparison() {
if (state.history.length === 0) return null;
const lastLife = state.history[state.history.length - 1];
const bestLife = state.history.reduce((best, current) =>
(current.age > best.age) ? current : best
, state.history[0]);
return {
vsLast: {
ageDiff: state.age - lastLife.age,
levelDiff: compareLevels(state.careers, lastLife.careers),
},
vsBest: {
ageDiff: state.age - bestLife.age,
levelDiff: compareLevels(state.careers, bestLife.careers),
},
};
}
function compareLevels(current, previous) {
const result = {};
const allIds = new Set([...Object.keys(current), ...Object.keys(previous)]);
for (const id of allIds) {
const curr = current[id]?.level || 0;
const prev = previous[id]?.level || 0;
result[id] = curr - prev;
}
return result;
}
// 重生
export function rebirth() {
resetStateForRebirth();
// 随机出身
state.family = Math.floor(Math.random() * 4) - 1; // -1 ~ 2
state.stats = {
body: Math.floor(Math.random() * 5) + 2, // 2~6,避免刚出生就死
wisdom: Math.floor(Math.random() * 5) + 2,
charm: Math.floor(Math.random() * 5) + 2,
luck: Math.floor(Math.random() * 5) + 2,
};
// 应用记忆效果
if (state.memories.includes('wisdom_boost')) state.stats.wisdom += 1;
if (state.memories.includes('body_boost')) state.stats.body += 1;
// 从已解锁词条池中随机抽取
const pool = Array.from(state.unlockedEntries);
state.talents = [];
const count = Math.min(3 + Math.floor(state.reincarnation / 10), 5);
for (let i = 0; i < count && pool.length > 0; i++) {
const idx = Math.floor(Math.random() * pool.length);
state.talents.push({ id: pool[idx] });
pool.splice(idx, 1);
}
addLog(`${state.reincarnation}世开始。出身: ${state.family > 0 ? '富贵' : state.family < 0 ? '贫寒' : '普通'}人家。`);
// 触发重生事件
if (typeof window !== 'undefined' && window.onRebirth) {
window.onRebirth();
}
}
+2
View File
@@ -36,6 +36,8 @@ export function pickRandomEvent(config, state, pool) {
if (event.trigger?.type !== 'random') return false; if (event.trigger?.type !== 'random') return false;
if (event.trigger.pool !== pool) return false; if (event.trigger.pool !== pool) return false;
if (state.triggeredEvents.has(event.id)) return false; if (state.triggeredEvents.has(event.id)) return false;
if (event.minAge !== undefined && state.age < event.minAge) return false;
if (event.maxAge !== undefined && state.age > event.maxAge) return false;
return true; return true;
}); });
+478
View File
@@ -0,0 +1,478 @@
// ==================== 经验系统 ====================
import { state, addLog } from './state.js';
// ==================== 职业配置 ====================
export const CAREERS = {
// 基础职业
hoodlum: {
id: 'hoodlum',
name: '混混',
type: 'short',
maxLevel: 10,
baseExp: 20,
expCurve: 1.15,
dailyExp: 5,
promotesTo: 'red_stick',
promoteRequirement: { level: 10 },
abilities: [
{ level: 3, id: 'street_fight', name: '街头斗殴', desc: '战斗事件成功率+10%' },
{ level: 7, id: 'intimidate', name: '威吓', desc: 'NPC事件中威慑选项可用' },
],
},
apprentice: {
id: 'apprentice',
name: '磨刀学徒',
type: 'short',
maxLevel: 10,
baseExp: 20,
expCurve: 1.15,
dailyExp: 5,
promotesTo: 'blade_helper',
promoteRequirement: { level: 10 },
abilities: [
{ level: 3, id: 'basic_smith', name: '基础锻造', desc: '锻造事件成功率+10%' },
{ level: 7, id: 'sharpen', name: '磨刀', desc: '武器类事件选项+1' },
],
},
farmer: {
id: 'farmer',
name: '农夫',
type: 'short',
maxLevel: 10,
baseExp: 15,
expCurve: 1.12,
dailyExp: 4,
promotesTo: 'hunter',
promoteRequirement: { level: 10 },
abilities: [
{ level: 3, id: 'plow', name: '耕作', desc: '体力恢复+10%' },
{ level: 7, id: 'harvest', name: '丰收', desc: '资源事件奖励+20%' },
],
},
student: {
id: 'student',
name: '书童',
type: 'short',
maxLevel: 10,
baseExp: 25,
expCurve: 1.18,
dailyExp: 6,
promotesTo: 'scholar',
promoteRequirement: { level: 10 },
abilities: [
{ level: 3, id: 'read', name: '阅读', desc: '悟性成长+10%' },
{ level: 7, id: 'memorize', name: '过目不忘', desc: '记忆解锁概率+10%' },
],
},
// 进阶职业
red_stick: {
id: 'red_stick',
name: '红棍',
type: 'short',
maxLevel: 15,
baseExp: 35,
expCurve: 1.18,
dailyExp: 8,
promotesTo: 'incense_master',
promoteRequirement: { level: 15 },
abilities: [
{ level: 5, id: 'gang_fight', name: '群架', desc: '多人战斗事件成功率+15%' },
{ level: 10, id: 'leadership', name: '领导力', desc: '可招募NPC随从' },
],
},
blade_helper: {
id: 'blade_helper',
name: '打刀下手',
type: 'short',
maxLevel: 15,
baseExp: 35,
expCurve: 1.18,
dailyExp: 8,
promotesTo: 'smith_master',
promoteRequirement: { level: 15 },
abilities: [
{ level: 5, id: 'temper', name: '淬火', desc: '武器品质+1' },
{ level: 10, id: 'pattern', name: '花纹', desc: '锻造事件奖励+25%' },
],
},
hunter: {
id: 'hunter',
name: '猎户',
type: 'short',
maxLevel: 15,
baseExp: 30,
expCurve: 1.16,
dailyExp: 7,
promotesTo: 'ranger',
promoteRequirement: { level: 15 },
abilities: [
{ level: 5, id: 'track', name: '追踪', desc: '野外事件成功率+15%' },
{ level: 10, id: 'trap', name: '陷阱', desc: '可设置陷阱事件' },
],
},
scholar: {
id: 'scholar',
name: '秀才',
type: 'medium',
maxLevel: 30,
baseExp: 40,
expCurve: 1.2,
dailyExp: 10,
promotesTo: 'juren',
promoteRequirement: { level: 30 },
abilities: [
{ level: 10, id: 'poetry', name: '诗词', desc: '魅力事件成功率+15%' },
{ level: 20, id: 'strategy', name: '谋略', desc: '政治事件选项+1' },
],
},
// 中阶职业
incense_master: {
id: 'incense_master',
name: '香主',
type: 'medium',
maxLevel: 30,
baseExp: 50,
expCurve: 1.2,
dailyExp: 12,
promotesTo: 'hall_master',
promoteRequirement: { level: 30 },
abilities: [
{ level: 10, id: 'network', name: '人脉', desc: 'NPC事件选项+2' },
{ level: 20, id: 'protection', name: '庇护', desc: '被攻击时减伤20%' },
],
},
smith_master: {
id: 'smith_master',
name: '锻刀师傅',
type: 'medium',
maxLevel: 30,
baseExp: 50,
expCurve: 1.2,
dailyExp: 12,
promotesTo: 'blade_saint',
promoteRequirement: { level: 30 },
abilities: [
{ level: 10, id: 'masterwork', name: '杰作', desc: '10%概率打造极品武器' },
{ level: 20, id: 'legendary', name: '传说', desc: '1%概率打造传说武器' },
],
},
ranger: {
id: 'ranger',
name: '游侠',
type: 'medium',
maxLevel: 30,
baseExp: 45,
expCurve: 1.18,
dailyExp: 11,
promotesTo: 'wuxia',
promoteRequirement: { level: 30 },
abilities: [
{ level: 10, id: 'archery', name: '箭术', desc: '远程攻击成功率+20%' },
{ level: 20, id: 'survival', name: '生存', desc: '野外生存事件自动成功' },
],
},
juren: {
id: 'juren',
name: '举人',
type: 'medium',
maxLevel: 30,
baseExp: 55,
expCurve: 1.22,
dailyExp: 13,
promotesTo: 'jinshi',
promoteRequirement: { level: 30 },
abilities: [
{ level: 10, id: 'governance', name: '治理', desc: '政治事件成功率+15%' },
{ level: 20, id: 'influence', name: '影响力', desc: '可发起政治事件' },
],
},
// 高阶职业
hall_master: {
id: 'hall_master',
name: '堂主',
type: 'long',
maxLevel: 50,
baseExp: 70,
expCurve: 1.22,
dailyExp: 15,
promotesTo: 'helmsman',
promoteRequirement: { level: 50 },
abilities: [
{ level: 15, id: 'dominate', name: '统御', desc: '可控制一片区域' },
{ level: 30, id: 'warlord', name: '军阀', desc: '战斗事件全员加成+20%' },
],
},
blade_saint: {
id: 'blade_saint',
name: '炼刀匠人',
type: 'long',
maxLevel: 50,
baseExp: 70,
expCurve: 1.22,
dailyExp: 15,
promotesTo: 'sword_immortal',
promoteRequirement: { level: 50 },
abilities: [
{ level: 15, id: 'sword_aura', name: '剑气', desc: '攻击附带剑气伤害' },
{ level: 30, id: 'blade_soul', name: '刀魂', desc: '武器可进化' },
],
},
wuxia: {
id: 'wuxia',
name: '侠客',
type: 'long',
maxLevel: 50,
baseExp: 65,
expCurve: 1.2,
dailyExp: 14,
promotesTo: 'sword_immortal',
promoteRequirement: { level: 50 },
abilities: [
{ level: 15, id: 'lightness', name: '轻功', desc: '移动速度+50%' },
{ level: 30, id: 'sword_qi', name: '剑气外放', desc: '攻击范围扩大' },
],
},
jinshi: {
id: 'jinshi',
name: '进士',
type: 'long',
maxLevel: 50,
baseExp: 75,
expCurve: 1.24,
dailyExp: 16,
promotesTo: 'prime_minister',
promoteRequirement: { level: 50 },
abilities: [
{ level: 15, id: 'policy', name: '政令', desc: '可改变外敌进度' },
{ level: 30, id: 'dynasty', name: '定国', desc: '王朝崩坏速度-30%' },
],
},
// 顶级职业
helmsman: {
id: 'helmsman',
name: '舵主',
type: 'long',
maxLevel: 80,
baseExp: 90,
expCurve: 1.25,
dailyExp: 18,
promotesTo: 'gang_leader',
promoteRequirement: { level: 80 },
abilities: [
{ level: 20, id: 'underworld', name: '地下王国', desc: '可控制多个城市' },
{ level: 50, id: 'immortal_body', name: '金刚不坏', desc: '体质+10' },
],
},
sword_immortal: {
id: 'sword_immortal',
name: '剑仙',
type: 'ultimate',
maxLevel: 100,
baseExp: 100,
expCurve: 1.25,
dailyExp: 20,
abilities: [
{ level: 25, id: 'flying_sword', name: '御剑飞行', desc: '可跨越区域' },
{ level: 50, id: 'immortal_sword', name: '仙剑', desc: '攻击无视防御' },
{ level: 80, id: 'dao', name: '剑道', desc: '可斩天魔' },
],
},
prime_minister: {
id: 'prime_minister',
name: '宰相',
type: 'long',
maxLevel: 80,
baseExp: 95,
expCurve: 1.25,
dailyExp: 19,
promotesTo: 'emperor',
promoteRequirement: { level: 80 },
abilities: [
{ level: 20, id: 'reform', name: '变法', desc: '可大幅改变外敌进度' },
{ level: 50, id: 'unify', name: '一统', desc: '三相外敌进度-10%' },
],
},
// 终极职业
gang_leader: {
id: 'gang_leader',
name: '帮主',
type: 'ultimate',
maxLevel: 100,
baseExp: 110,
expCurve: 1.25,
dailyExp: 22,
abilities: [
{ level: 30, id: 'supreme', name: '至尊', desc: '所有帮派事件自动成功' },
{ level: 60, id: 'legend', name: '传说', desc: '可招募传说级NPC' },
],
},
emperor: {
id: 'emperor',
name: '帝王',
type: 'ultimate',
maxLevel: 100,
baseExp: 110,
expCurve: 1.25,
dailyExp: 22,
abilities: [
{ level: 30, id: 'mandate', name: '天命', desc: '王朝永不崩坏' },
{ level: 60, id: 'sage_king', name: '圣王', desc: '三相外敌进度-20%' },
],
},
};
// ==================== 经验计算 ====================
export function gainExp(careerId, amount) {
const config = CAREERS[careerId];
if (!config) return 0;
if (!state.careers[careerId]) {
state.careers[careerId] = { level: 0, exp: 0, active: false, unlockedAbilities: [] };
}
const career = state.careers[careerId];
const multiplier = getExpMultiplier(careerId);
const gain = Math.floor(amount * multiplier);
career.exp += gain;
// 检查升级
while (career.level < config.maxLevel) {
const need = Math.floor(config.baseExp * Math.pow(config.expCurve, career.level));
if (career.exp >= need) {
career.exp -= need;
career.level++;
addLog(`${config.name}提升至 Lv.${career.level}`);
// 解锁能力
for (const ability of config.abilities || []) {
if (ability.level === career.level && !career.unlockedAbilities.includes(ability.id)) {
career.unlockedAbilities.push(ability.id);
addLog(`解锁能力:${ability.name} - ${ability.desc}`, 'ability');
}
}
// 检查晋升
if (config.promotesTo && career.level >= config.promoteRequirement?.level) {
const nextConfig = CAREERS[config.promotesTo];
if (nextConfig) {
addLog(`${config.name}已圆满,可晋升至${nextConfig.name}`, 'promote');
}
}
} else {
break;
}
}
// 封顶
if (career.level >= config.maxLevel) {
career.level = config.maxLevel;
career.exp = 0;
}
return gain;
}
export function gainDailyExp() {
for (const [careerId, career] of Object.entries(state.careers)) {
if (!career.active) continue;
const config = CAREERS[careerId];
if (!config) continue;
gainExp(careerId, config.dailyExp);
}
}
export function getExpMultiplier(careerId) {
let multiplier = 1;
const config = CAREERS[careerId];
const career = state.careers[careerId];
const level = career?.level || 0;
// 元经验加成(对数衰减,永不归零)
const meta = state.metaExp[careerId] || 0;
const metaBoost = Math.log(1 + meta / 100) * Math.exp(-level / 80);
multiplier += metaBoost;
// 天赋加成
for (const t of state.talents) {
if (t.effect?.target === careerId || t.effect?.target === 'all') {
multiplier += t.effect.value;
}
if (t.effect?.target === 'short' && config?.type === 'short') {
multiplier += t.effect.value;
}
}
// 记忆加成
for (const m of state.memories) {
if (m.target === careerId) {
multiplier += m.value;
}
}
// 路线疲劳(同一职业连续多世,收益递减)
const recentHistory = state.history.slice(-3);
const sameCareerCount = recentHistory.filter(h =>
h.careers[careerId] && h.careers[careerId].level >= 5
).length;
if (sameCareerCount >= 3) {
multiplier *= 0.7; // 连续3世同职业,收益-30%
}
// 新职业首世加成
if (career?.level === 0 && meta < 100) {
multiplier *= 2.0;
}
return multiplier;
}
export function setActiveCareer(careerId) {
// 取消所有激活
for (const id in state.careers) {
state.careers[id].active = false;
}
// 创建如果不存在
if (!state.careers[careerId]) {
state.careers[careerId] = { level: 0, exp: 0, active: true, unlockedAbilities: [] };
} else {
state.careers[careerId].active = true;
}
}
export function getCareerPath(careerId) {
const path = [];
let current = careerId;
while (current) {
const config = CAREERS[current];
if (!config) break;
path.unshift(config);
// 反向查找前置职业
current = Object.entries(CAREERS).find(([_, c]) => c.promotesTo === current)?.[0];
}
return path;
}
export function getUnlockableCareers() {
const unlockable = [];
for (const [id, config] of Object.entries(CAREERS)) {
if (state.careers[id]) continue; // 已解锁
if (!config.promoteRequirement) continue; // 基础职业
// 检查前置职业是否达标
const prerequisiteId = Object.entries(CAREERS).find(([_, c]) => c.promotesTo === id)?.[0];
if (prerequisiteId && state.careers[prerequisiteId]?.level >= config.promoteRequirement.level) {
unlockable.push(config);
}
}
return unlockable;
}
+143
View File
@@ -0,0 +1,143 @@
// ==================== 存档系统 ====================
import { state } from './state.js';
const SAVE_KEY = 'rebirth_incremental_save';
const HISTORY_KEY = 'rebirth_incremental_history';
export function saveGame(slot = 'auto') {
const saveData = {
version: '0.1',
slot,
timestamp: Date.now(),
state: serializeState(),
};
const key = `${SAVE_KEY}_${slot}`;
localStorage.setItem(key, JSON.stringify(saveData));
// 历史记录单独存
localStorage.setItem(HISTORY_KEY, JSON.stringify(state.history));
return true;
}
export function loadGame(slot = 'auto') {
const key = `${SAVE_KEY}_${slot}`;
const data = localStorage.getItem(key);
if (!data) return null;
try {
const saveData = JSON.parse(data);
if (saveData.version !== '0.1') {
console.warn('存档版本不匹配');
return null;
}
deserializeState(saveData.state);
// 加载历史
const historyData = localStorage.getItem(HISTORY_KEY);
if (historyData) {
state.history = JSON.parse(historyData);
}
return saveData;
} catch (e) {
console.error('读档失败', e);
return null;
}
}
export function hasSave(slot = 'auto') {
return !!localStorage.getItem(`${SAVE_KEY}_${slot}`);
}
export function deleteSave(slot = 'auto') {
localStorage.removeItem(`${SAVE_KEY}_${slot}`);
}
export function exportSave() {
const saveData = {
version: '0.1',
state: serializeState(),
history: state.history,
exportTime: Date.now(),
};
return btoa(JSON.stringify(saveData));
}
export function importSave(base64Str) {
try {
const data = JSON.parse(atob(base64Str));
if (data.version !== '0.1') {
throw new Error('版本不匹配');
}
deserializeState(data.state);
if (data.history) {
state.history = data.history;
}
return true;
} catch (e) {
console.error('导入失败', e);
return false;
}
}
function serializeState() {
return {
reincarnation: state.reincarnation,
day: state.day,
year: state.year,
age: state.age,
alive: state.alive,
talents: state.talents,
stats: state.stats,
family: state.family,
careers: state.careers,
memories: state.memories,
metaExp: state.metaExp,
unlockedEntries: Array.from(state.unlockedEntries),
worldFlags: state.worldFlags,
npcs: state.npcs,
enemyProgress: state.enemyProgress,
enemyPeak: state.enemyPeak,
hiddenFourthRevealed: state.hiddenFourthRevealed,
crisisLevel: state.crisisLevel,
speed: state.speed,
paused: state.paused,
waitingChoice: state.waitingChoice,
currentEvent: state.currentEvent,
logs: state.logs.slice(0, 100), // 只存最近100条
triggeredEvents: Array.from(state.triggeredEvents),
};
}
function deserializeState(data) {
Object.assign(state, {
reincarnation: data.reincarnation || 0,
day: data.day || 0,
year: data.year || 0,
age: data.age || 0,
alive: data.alive !== false,
talents: data.talents || [],
stats: data.stats || { body: 0, wisdom: 0, charm: 0, luck: 0 },
family: data.family || 0,
careers: data.careers || {},
memories: data.memories || [],
metaExp: data.metaExp || {},
unlockedEntries: new Set(data.unlockedEntries || []),
worldFlags: data.worldFlags || {},
npcs: data.npcs || {},
enemyProgress: data.enemyProgress || { military: 0, spiritual: 0, political: 0 },
enemyPeak: data.enemyPeak || { military: 0, spiritual: 0, political: 0 },
hiddenFourthRevealed: data.hiddenFourthRevealed || false,
crisisLevel: data.crisisLevel || 0,
speed: data.speed || 0,
paused: data.paused || false,
waitingChoice: data.waitingChoice || false,
currentEvent: data.currentEvent || null,
logs: data.logs || [],
triggeredEvents: new Set(data.triggeredEvents || []),
});
}
+13 -9
View File
@@ -48,24 +48,28 @@ export function tick(state, days, careerConfig, eventConfig, shopConfig) {
// 8. 检查年龄触发事件 // 8. 检查年龄触发事件
const ageEvents = checkAgeEvents(eventConfig, state); const ageEvents = checkAgeEvents(eventConfig, state);
for (const event of ageEvents) { for (const event of ageEvents) {
if (event.isChoice) { const ev = { ...event, title: event.name || event.title, text: event.description || event.text };
if (ev.isChoice) {
state.paused = true; state.paused = true;
state.currentEvent = event; state.currentEvent = ev;
return; return;
} else { } else {
executeEvent(event, state, applyEffect); executeEvent(ev, state, applyEffect);
} }
} }
// 9. 随机事件(30%概率) // 9. 随机事件(30%概率)
if (Math.random() < 0.3) { if (Math.random() < 0.3) {
const randomEvent = pickRandomEvent(eventConfig, state, 'normal'); const randomEvent = pickRandomEvent(eventConfig, state, 'normal');
if (randomEvent && !randomEvent.isChoice) { if (randomEvent) {
executeEvent(randomEvent, state, applyEffect); const ev = { ...randomEvent, title: randomEvent.name || randomEvent.title, text: randomEvent.description || randomEvent.text };
} else if (randomEvent && randomEvent.isChoice) { if (!ev.isChoice) {
state.paused = true; executeEvent(ev, state, applyEffect);
state.currentEvent = randomEvent; } else {
return; state.paused = true;
state.currentEvent = ev;
return;
}
} }
} }
+292
View File
@@ -0,0 +1,292 @@
// ==================== 时间推进系统 ====================
import { state, addLog } from './state.js';
import { eventEngine } from './eventEngine.js';
import { checkDeath } from './death.js';
import { gainDailyExp } from './expSystem.js';
import { renderAll } from '../ui/renderer.js';
// 加速档位:天数/秒
export const SPEED_PRESETS = [
{ label: '1x', daysPerSecond: 1 },
{ label: '10x', daysPerSecond: 10 },
{ label: '100x', daysPerSecond: 100 },
{ label: 'MAX', daysPerSecond: 1000 },
];
let tickInterval = null;
let lastTickTime = 0;
// 事件密度配置(基于秒级间隔)
const EVENT_DENSITY = {
normalText: { intervalSec: 1, weight: 100 }, // 普通文本事件
impactText: { intervalSec: 5, weight: 30 }, // 影响文本事件
majorEvent: { intervalSec: 120, weight: 10 }, // 大事件/选择事件
};
// 计算当前有效的事件间隔(受加速档位、职业、事件链影响)
function getEffectiveInterval(type, speedLevel) {
const base = EVENT_DENSITY[type].intervalSec;
const speed = SPEED_PRESETS[speedLevel]?.daysPerSecond || 1;
// 加速档位越高,事件间隔越短(但不超过上限)
let multiplier = 1;
if (speed >= 100) multiplier = 0.3;
else if (speed >= 10) multiplier = 0.6;
// 职业影响:某些职业触发更多事件
const activeCareers = Object.entries(state.careers).filter(([_, c]) => c.active);
for (const [id, _] of activeCareers) {
if (id === 'hoodlum' || id === 'wuxia') {
multiplier *= 0.8; // 江湖职业事件更多
}
}
// 事件链活跃时,大事件间隔缩短
if (type === 'majorEvent' && eventEngine.hasActiveChains()) {
multiplier *= 0.5;
}
return base * multiplier;
}
// 累积的事件概率(用于批次触发)
let eventAccumulator = {
normalText: 0,
impactText: 0,
majorEvent: 0,
};
export function tick(days = 1) {
if (!state.alive || state.paused) return;
for (let i = 0; i < days; i++) {
state.day++;
// 年龄增长(每365天1岁)
if (state.day % 365 === 0) {
state.year++;
state.age++;
// 每年自动存档
if (typeof saveGame === 'function') {
saveGame('auto');
}
}
// 每天自然成长
gainDailyExp();
// 推进外敌
advanceEnemy();
// 更新危机感
updateCrisisLevel();
// 检查隐藏第四相(在危机感更新后)
checkHiddenFourth();
// 属性下限保护(避免负数导致立刻死亡)
for (const key of Object.keys(state.stats)) {
if (state.stats[key] < 0.5) state.stats[key] = 0.5;
}
// 事件触发(累积概率)
triggerEvents();
// 检查死亡
if (checkDeath()) {
return;
}
}
}
function triggerEvents() {
// 普通文本事件(~1秒间隔)
eventAccumulator.normalText += 1 / getEffectiveInterval('normalText', state.speed);
if (eventAccumulator.normalText >= 1) {
eventAccumulator.normalText -= 1;
const ev = eventEngine.pickEvent('normalText');
if (ev) executeEvent(ev);
}
// 影响文本事件(~5秒间隔)
eventAccumulator.impactText += 1 / getEffectiveInterval('impactText', state.speed);
if (eventAccumulator.impactText >= 1) {
eventAccumulator.impactText -= 1;
const ev = eventEngine.pickEvent('impactText');
if (ev) executeEvent(ev);
}
// 大事件/选择事件(~120秒间隔)
eventAccumulator.majorEvent += 1 / getEffectiveInterval('majorEvent', state.speed);
if (eventAccumulator.majorEvent >= 1) {
eventAccumulator.majorEvent -= 1;
const ev = eventEngine.pickEvent('majorEvent');
if (ev) executeEvent(ev);
}
}
function executeEvent(eventDef) {
if (eventDef.isChoice) {
// 选择事件:暂停,等待玩家
state.paused = true;
state.waitingChoice = true;
state.currentEvent = eventDef;
// 触发UI渲染
if (typeof window !== 'undefined' && window.onChoiceEvent) {
window.onChoiceEvent(eventDef);
}
} else {
// 文本事件:自动执行
const result = eventDef.effect ? eventDef.effect(state) : {};
addLog(eventDef.text || eventDef.title, eventDef.impact > 0.5 ? 'impact' : 'normal');
// 应用事件效果
if (result.exp) {
import('./expSystem.js').then(m => {
for (const [careerId, amount] of Object.entries(result.exp)) {
m.gainExp(careerId, amount);
}
});
}
if (result.flags) {
Object.assign(state.worldFlags, result.flags);
}
}
// 属性下限保护(事件可能扣减属性)
for (const key of Object.keys(state.stats)) {
if (state.stats[key] < 0.5) state.stats[key] = 0.5;
}
state.triggeredEvents.add(eventDef.id);
}
function advanceEnemy() {
// 基础增长
state.enemyProgress.military += Math.random() * 0.02;
state.enemyProgress.spiritual += Math.random() * 0.015;
state.enemyProgress.political += Math.random() * 0.01;
// 联动:王朝崩坏加速蛮族
if (state.enemyProgress.political > 30) {
state.enemyProgress.military += 0.01;
}
// 蛮族入侵加速民心下降
if (state.enemyProgress.military > 40) {
state.enemyProgress.political += 0.008;
}
// 天魔侵蚀导致修仙者死亡 → 天魔加速
if (state.enemyProgress.spiritual > 50) {
state.enemyProgress.spiritual += 0.005;
}
// 封顶
for (const key of Object.keys(state.enemyProgress)) {
state.enemyProgress[key] = Math.min(100, state.enemyProgress[key]);
}
// 更新历史峰值
for (const key of Object.keys(state.enemyProgress)) {
state.enemyPeak[key] = Math.max(state.enemyPeak[key] || 0, state.enemyProgress[key]);
}
}
function checkHiddenFourth() {
// 检查隐藏第四相:经历过多世轮回后,在相对安全的时刻顿悟真相
if (!state.hiddenFourthRevealed &&
state.reincarnation >= 3 &&
state.age >= 15 &&
state.enemyPeak.military >= 25 &&
state.enemyPeak.spiritual >= 25 &&
state.enemyPeak.political >= 25 &&
state.crisisLevel < 50) {
state.hiddenFourthRevealed = true;
addLog('【重大发现】三相稳定的背后,似乎有什么东西在注视着你...', 'major');
addLog('你意识到:真正的敌人从来不是蛮族、天魔或王朝——而是这无尽的轮回本身。', 'major');
}
}
function updateCrisisLevel() {
let crisis = 0;
// 年龄危机感
if (state.age > 50) {
crisis += (state.age - 50) * 0.5;
}
// 外敌危机感
const maxEnemy = Math.max(...Object.values(state.enemyProgress));
crisis += maxEnemy * 0.3;
// 体质危机感
if (state.stats.body < 3) {
crisis += (3 - state.stats.body) * 5;
}
state.crisisLevel = Math.min(100, crisis);
// 危机感高时,日志提示
if (state.crisisLevel > 80 && Math.random() < 0.1) {
addLog('你感到一阵强烈的不安...', 'crisis');
}
}
// 启动/停止时间循环
export function startGameLoop() {
if (tickInterval) return;
const speed = SPEED_PRESETS[state.speed]?.daysPerSecond || 1;
const intervalMs = 1000 / speed;
tickInterval = setInterval(() => {
if (!state.paused && state.alive) {
tick(1);
renderAll();
}
}, intervalMs);
}
export function stopGameLoop() {
if (tickInterval) {
clearInterval(tickInterval);
tickInterval = null;
}
}
export function setSpeed(level) {
state.speed = level;
stopGameLoop();
startGameLoop();
}
export function pause() {
state.paused = true;
}
export function resume() {
state.paused = false;
}
// 选择事件倒计时
let choiceTimer = null;
export function startChoiceTimer(eventDef, onTimeout) {
if (choiceTimer) clearTimeout(choiceTimer);
const timeoutMs = 10000; // 10秒
choiceTimer = setTimeout(() => {
// 自动选默认选项
const defaultIdx = eventDef.defaultChoice || 0;
onTimeout(defaultIdx);
}, timeoutMs);
return () => {
if (choiceTimer) {
clearTimeout(choiceTimer);
choiceTimer = null;
}
};
}
+161 -8
View File
@@ -8,9 +8,59 @@ import { startGameLoop, stopGameLoop, setSpeed } from './engine/tickLoop.js';
import { applyEffect } from './engine/effectApplier.js'; import { applyEffect } from './engine/effectApplier.js';
import { processDeath } from './engine/deathEngine.js'; import { processDeath } from './engine/deathEngine.js';
import { evaluateCondition } from './engine/conditionEvaluator.js'; import { evaluateCondition } from './engine/conditionEvaluator.js';
import { initRenderer, renderAll, showDeathScreen, hideChoiceEvent } from './ui/renderer.js'; import { buyItem, canBuyItem } from './engine/shopEngine.js';
import {
initRenderer, renderAll, showDeathScreen, hideChoiceEvent,
hideTitleScreen, showToast, updatePauseButton, updateSpeedButton, updateTimer
} from './ui/renderer.js';
const state = createInitialState(); const state = createInitialState();
let _configs = null;
let _gameStarted = false;
let _soundEnabled = false;
let _bgmAudio = null;
let _bgmBattle = null;
let _eventTimerId = null;
let _eventSecondsLeft = 0;
function getOnTick() {
return () => renderAll(state, _configs.careers, _configs.shop, _configs.talents);
}
// ==================== 音频 ====================
function initAudio() {
_bgmAudio = new Audio('assets/audio/bgm_main.mp3');
_bgmAudio.loop = true;
_bgmAudio.volume = 0.4;
_bgmBattle = new Audio('assets/audio/bgm_battle.mp3');
_bgmBattle.loop = true;
_bgmBattle.volume = 0.4;
}
function toggleSound() {
_soundEnabled = !_soundEnabled;
const btn = document.getElementById('sound-btn');
if (btn) btn.textContent = _soundEnabled ? '🔊' : '🔇';
if (_soundEnabled) {
_bgmAudio?.play().catch(() => {});
} else {
_bgmAudio?.pause();
_bgmBattle?.pause();
}
}
function playBattleMusic() {
if (!_soundEnabled) return;
_bgmAudio?.pause();
_bgmBattle?.play().catch(() => {});
}
function playMainMusic() {
if (!_soundEnabled) return;
_bgmBattle?.pause();
_bgmAudio?.play().catch(() => {});
}
// ==================== 配置加载 ==================== // ==================== 配置加载 ====================
@@ -32,8 +82,9 @@ async function loadConfigs() {
function startNewGame(configs) { function startNewGame(configs) {
resetForRebirth(state); resetForRebirth(state);
state.speed = state.speed || 0;
updateSpeedButton(state.speed);
// 随机选择可用身份
const available = configs.identities.identities.filter(id => { const available = configs.identities.identities.filter(id => {
if (!id.unlockConditions || id.unlockConditions.length === 0) return true; if (!id.unlockConditions || id.unlockConditions.length === 0) return true;
return id.unlockConditions.every(c => evaluateCondition(c, state)); return id.unlockConditions.every(c => evaluateCondition(c, state));
@@ -48,11 +99,52 @@ function startNewGame(configs) {
} }
} }
// 从已解锁词条池抽取(简化:先不实现) startGameLoop(state, configs.careers, configs.events, configs.shop, getOnTick());
playMainMusic();
}
startGameLoop(state, configs.careers, configs.events, configs.shop, () => { // ==================== 事件倒计时 ====================
renderAll(state, configs.careers, configs.shop);
}); function startEventTimer() {
clearEventTimer();
_eventSecondsLeft = 10;
updateTimer(_eventSecondsLeft);
_eventTimerId = setInterval(() => {
_eventSecondsLeft--;
updateTimer(_eventSecondsLeft);
if (_eventSecondsLeft <= 0) {
clearEventTimer();
// 自动选择第一个可用选项,否则选最后一个
autoSelectChoice();
}
}, 1000);
}
function clearEventTimer() {
if (_eventTimerId) {
clearInterval(_eventTimerId);
_eventTimerId = null;
}
_eventSecondsLeft = 0;
updateTimer(0);
}
function autoSelectChoice() {
if (!state.currentEvent || !state.currentEvent.choices) {
state.paused = false;
state.currentEvent = null;
hideChoiceEvent();
return;
}
const choices = state.currentEvent.choices;
let chosenIdx = choices.length - 1;
for (let i = 0; i < choices.length; i++) {
if (!choices[i].requirements || evaluateCondition(choices[i].requirements, state)) {
chosenIdx = i;
break;
}
}
handleChoice(chosenIdx);
} }
// ==================== 选择事件处理 ==================== // ==================== 选择事件处理 ====================
@@ -66,17 +158,48 @@ function handleChoice(choiceIndex) {
} }
state.paused = false; state.paused = false;
state.currentEvent = null; state.currentEvent = null;
clearEventTimer();
hideChoiceEvent(); hideChoiceEvent();
} }
// ==================== 商铺购买 ====================
function handleShopBuy(itemId) {
if (!_configs) return;
if (!canBuyItem(_configs.shop, state, itemId)) {
showToast('银两不足或条件不满足');
return;
}
const result = buyItem(_configs.shop, state, itemId);
if (result) {
showToast('购买成功');
renderAll(state, _configs.careers, _configs.shop, _configs.talents);
} else {
showToast('购买失败');
}
}
// ==================== UI事件绑定 ==================== // ==================== UI事件绑定 ====================
function bindUIEvents() { function bindUIEvents() {
// 标题画面开始
const titleStartBtn = document.getElementById('title-start-btn');
if (titleStartBtn) {
titleStartBtn.addEventListener('click', () => {
hideTitleScreen();
if (!_gameStarted) {
_gameStarted = true;
startNewGame(_configs);
}
});
}
// 速度按钮 // 速度按钮
document.querySelectorAll('.speed-btn').forEach(btn => { document.querySelectorAll('.speed-btn').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const level = parseInt(btn.dataset.speed, 10); const level = parseInt(btn.dataset.speed, 10);
setSpeed(state, level); setSpeed(state, level, _configs.careers, _configs.events, _configs.shop, getOnTick());
updateSpeedButton(level);
}); });
}); });
@@ -85,9 +208,16 @@ function bindUIEvents() {
if (pauseBtn) { if (pauseBtn) {
pauseBtn.addEventListener('click', () => { pauseBtn.addEventListener('click', () => {
state.paused = !state.paused; state.paused = !state.paused;
updatePauseButton(state.paused);
}); });
} }
// 声音按钮
const soundBtn = document.getElementById('sound-btn');
if (soundBtn) {
soundBtn.addEventListener('click', toggleSound);
}
// 选择按钮委托 // 选择按钮委托
const eventChoices = document.getElementById('event-choices'); const eventChoices = document.getElementById('event-choices');
if (eventChoices) { if (eventChoices) {
@@ -101,6 +231,19 @@ function bindUIEvents() {
}); });
} }
// 商铺购买委托
const shopPanel = document.getElementById('shop-panel');
if (shopPanel) {
shopPanel.addEventListener('click', (e) => {
const btn = e.target.closest('.shop-buy-btn');
if (!btn) return;
const itemId = btn.dataset.itemId;
if (itemId) {
handleShopBuy(itemId);
}
});
}
// 重生按钮(死亡界面) // 重生按钮(死亡界面)
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (e.target.id === 'rebirth-btn') { if (e.target.id === 'rebirth-btn') {
@@ -113,10 +256,20 @@ function bindUIEvents() {
async function init() { async function init() {
const configs = await loadConfigs(); const configs = await loadConfigs();
_configs = configs;
initRenderer(); initRenderer();
initAudio();
bindUIEvents(); bindUIEvents();
startNewGame(configs); let _hadEvent = false;
setInterval(() => { setInterval(() => {
// 检查事件触发,启动倒计时
if (state.currentEvent && !_hadEvent) {
_hadEvent = true;
startEventTimer();
} else if (!state.currentEvent && _hadEvent) {
_hadEvent = false;
clearEventTimer();
}
if (!state.alive && state.deathReason) { if (!state.alive && state.deathReason) {
stopGameLoop(); stopGameLoop();
processDeath(state, configs.careers, configs.talents); processDeath(state, configs.careers, configs.talents);
+149 -69
View File
@@ -1,6 +1,6 @@
// ==================== UI 渲染器 ==================== // ==================== UI 渲染器 ====================
import { getEntry } from '../content/talents.js'; import { evaluateCondition } from '../engine/conditionEvaluator.js';
// DOM 元素缓存 // DOM 元素缓存
let els = {}; let els = {};
@@ -8,14 +8,25 @@ let els = {};
// 商铺当前 Tab // 商铺当前 Tab
let shopActiveTab = 'items'; let shopActiveTab = 'items';
// 上次数值(用于变化检测)
let lastStats = {};
let lastLogCount = 0;
let lastEventId = null;
export function initRenderer() { export function initRenderer() {
els = { els = {
titleScreen: document.getElementById('title-screen'),
titleStartBtn: document.getElementById('title-start-btn'),
gameContainer: document.getElementById('game-container'),
eventBgImage: document.getElementById('event-bg-image'),
toast: document.getElementById('toast'),
reincarnation: document.getElementById('reincarnation'), reincarnation: document.getElementById('reincarnation'),
year: document.getElementById('year'), year: document.getElementById('year'),
day: document.getElementById('day'), day: document.getElementById('day'),
age: document.getElementById('age'), age: document.getElementById('age'),
money: document.getElementById('money'), money: document.getElementById('money'),
crisis: document.getElementById('crisis'), crisis: document.getElementById('crisis'),
crisisBar: document.getElementById('crisis-bar'),
stats: { stats: {
body: document.getElementById('stat-body'), body: document.getElementById('stat-body'),
wisdom: document.getElementById('stat-wisdom'), wisdom: document.getElementById('stat-wisdom'),
@@ -37,33 +48,13 @@ export function initRenderer() {
shopTabs: document.getElementById('shop-tabs'), shopTabs: document.getElementById('shop-tabs'),
deathScreen: document.getElementById('death-screen'), deathScreen: document.getElementById('death-screen'),
deathContent: document.getElementById('death-content'), deathContent: document.getElementById('death-content'),
memoryList: document.getElementById('memory-list'),
metaExpList: document.getElementById('meta-exp-list'),
}; };
injectShopStyles();
initShopTabs(); initShopTabs();
} }
function injectShopStyles() {
if (document.getElementById('renderer-shop-styles')) return;
const style = document.createElement('style');
style.id = 'renderer-shop-styles';
style.textContent = `
.shop-tabs { display: flex; gap: 10px; margin-bottom: 10px; }
.shop-tab { padding: 5px 15px; cursor: pointer; border: none; background: #1e3a5f; color: #ccc; border-radius: 4px; font-size: 13px; }
.shop-tab.active { background: #f0c040; color: #1a1a2e; }
.shop-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.shop-item { background: #0f1535; padding: 10px; border-radius: 6px; }
.shop-item.locked { opacity: 0.5; }
.shop-item-name { font-size: 14px; color: #f0c040; margin-bottom: 4px; }
.shop-item-cost { font-size: 12px; color: #e0c060; margin-bottom: 4px; }
.shop-item-desc { font-size: 12px; color: #888; }
.artifact-item { background: #0f1535; padding: 8px 12px; border-radius: 6px; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; }
.artifact-name { color: #f0c040; font-size: 14px; }
.artifact-level { color: #e0c060; font-size: 13px; }
`;
document.head.appendChild(style);
}
function initShopTabs() { function initShopTabs() {
if (!els.shopTabs) return; if (!els.shopTabs) return;
els.shopTabs.innerHTML = ` els.shopTabs.innerHTML = `
@@ -78,27 +69,29 @@ function initShopTabs() {
els.shopTabs.querySelectorAll('.shop-tab').forEach(btn => { els.shopTabs.querySelectorAll('.shop-tab').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === shopActiveTab); btn.classList.toggle('active', btn.dataset.tab === shopActiveTab);
}); });
// Trigger re-render if state is available via a stored reference
if (els._lastState && els._lastCareerConfig && els._lastShopConfig) { if (els._lastState && els._lastCareerConfig && els._lastShopConfig) {
renderShop(els._lastState, els._lastShopConfig); renderShop(els._lastState, els._lastShopConfig);
} }
}); });
} }
export function renderAll(state, careerConfig, shopConfig) { export function renderAll(state, careerConfig, shopConfig, talentsConfig) {
// Store references for tab switching
els._lastState = state; els._lastState = state;
els._lastCareerConfig = careerConfig; els._lastCareerConfig = careerConfig;
els._lastShopConfig = shopConfig; els._lastShopConfig = shopConfig;
els._lastTalentsConfig = talentsConfig;
renderTopBar(state); renderTopBar(state);
renderStats(state); renderStats(state);
renderCareers(state, careerConfig); renderCareers(state, careerConfig);
renderTalents(state); renderTalents(state, talentsConfig);
renderArtifacts(state, shopConfig); renderArtifacts(state, shopConfig);
renderShop(state, shopConfig); renderShop(state, shopConfig);
renderLogs(state); renderLogs(state);
renderEventPanel(state); renderEventPanel(state);
renderMemories(state);
renderMetaExp(state, careerConfig);
renderCrisis(state);
} }
export function renderTopBar(state) { export function renderTopBar(state) {
@@ -106,17 +99,32 @@ export function renderTopBar(state) {
if (els.year) els.year.textContent = state.year || 0; if (els.year) els.year.textContent = state.year || 0;
if (els.day) els.day.textContent = state.day || 0; if (els.day) els.day.textContent = state.day || 0;
if (els.age) els.age.textContent = state.age || 0; if (els.age) els.age.textContent = state.age || 0;
if (els.money) els.money.textContent = state.money || 0; if (els.money) els.money.textContent = Math.floor(state.money || 0);
}
function renderCrisis(state) {
const crisis = state.crisis || 0;
const maxCrisis = 100;
if (els.crisis) els.crisis.textContent = crisis;
if (els.crisisBar) els.crisisBar.style.width = Math.min(100, (crisis / maxCrisis) * 100) + '%';
} }
export function renderStats(state) { export function renderStats(state) {
const stats = state.stats || {}; const stats = state.stats || {};
if (els.stats.body) els.stats.body.textContent = formatStat(stats.body); const keys = ['body', 'wisdom', 'charm', 'destiny', 'business', 'intelligence'];
if (els.stats.wisdom) els.stats.wisdom.textContent = formatStat(stats.wisdom); for (const key of keys) {
if (els.stats.charm) els.stats.charm.textContent = formatStat(stats.charm); const el = els.stats[key];
if (els.stats.destiny) els.stats.destiny.textContent = formatStat(stats.destiny); if (!el) continue;
if (els.stats.business) els.stats.business.textContent = formatStat(stats.business); const newVal = stats[key] ?? 0;
if (els.stats.intelligence) els.stats.intelligence.textContent = formatStat(stats.intelligence); const oldVal = lastStats[key] ?? 0;
el.textContent = formatStat(newVal);
if (Math.abs(newVal - oldVal) > 0.01) {
el.classList.remove('changed');
void el.offsetWidth;
el.classList.add('changed');
}
}
lastStats = { ...stats };
} }
function formatStat(value) { function formatStat(value) {
@@ -149,19 +157,20 @@ export function renderCareers(state, careerConfig) {
return ` return `
<div class="career-item"> <div class="career-item">
<span class="career-icon">&#9874;</span>
<div class="career-name">${c.name}</div> <div class="career-name">${c.name}</div>
<div class="career-level">Lv.${level}</div> <div class="career-level">Lv.${level}</div>
<div class="career-bar-wrap"> <div class="career-bar-wrap">
<div class="career-bar" style="width: ${pct}%"></div> <div class="career-bar" style="width: ${pct}%"></div>
<div class="career-exp-text">${exp}/${need}</div> <div class="career-exp-text">${exp}/${need}</div>
</div> </div>
<div style="width: 80px; text-align: right; font-size: 12px; color: #2ecc71;">+${income}两/天</div> <div class="career-income">+${income}两/天</div>
</div> </div>
`; `;
}).join(''); }).join('');
} }
export function renderTalents(state) { export function renderTalents(state, talentsConfig) {
if (!els.talentList) return; if (!els.talentList) return;
const talents = state.talents || []; const talents = state.talents || [];
@@ -170,11 +179,13 @@ export function renderTalents(state) {
return; return;
} }
els.talentList.innerHTML = talents.map(t => { const configList = talentsConfig?.talents || [];
const entry = getEntry(t.id);
els.talentList.innerHTML = talents.map(tid => {
const entry = configList.find(t => t.id === tid);
if (!entry) return ''; if (!entry) return '';
const typeClass = entry.type || 'talent'; const typeClass = entry.type || 'talent';
return `<span class="talent-tag ${typeClass}" title="${entry.desc || ''}">${entry.name}</span>`; return `<span class="talent-tag ${typeClass}" title="${entry.description || ''}">${entry.name}</span>`;
}).join(''); }).join('');
} }
@@ -244,27 +255,32 @@ export function renderShop(state, shopConfig) {
els.shopPanel.innerHTML = `<div class="shop-list">${items.map(item => { els.shopPanel.innerHTML = `<div class="shop-list">${items.map(item => {
const isLocked = isShopItemLocked(item, state); const isLocked = isShopItemLocked(item, state);
const canAfford = item.displayCost != null && state.money >= item.displayCost;
const isMaxLevel = item.displayCost == null;
const canPurchase = !isLocked && !isMaxLevel && canAfford;
const desc = buildEffectDesc(item.effects); const desc = buildEffectDesc(item.effects);
const costText = item.displayCost != null ? `${item.displayCost}` : '已 max'; const costText = item.displayCost != null ? `${item.displayCost}` : '已满级';
const levelText = item.section === 'artifact' && item.currentLevel != null const levelText = item.section === 'artifact' && item.currentLevel != null
? ` (当前 Lv.${item.currentLevel})` ? ` (Lv.${item.currentLevel})`
: ''; : '';
return ` return `
<div class="shop-item ${isLocked ? 'locked' : ''}"> <div class="shop-item ${isLocked || !canPurchase ? 'locked' : ''}">
<div class="shop-item-name">${item.name}${levelText}</div> <div class="shop-item-info">
<div class="shop-item-cost">${costText}</div> <div class="shop-item-name">${item.name}${levelText}</div>
<div class="shop-item-desc">${desc || '无描述'}</div> <div class="shop-item-desc">${desc || '无描述'}</div>
</div>
<div class="shop-item-cost ${!isLocked && !canAfford && !isMaxLevel ? 'insufficient' : ''}">${costText}</div>
<button class="shop-buy-btn" data-item-id="${item.id}" data-section="${item.section}" ${!canPurchase ? 'disabled' : ''}>
${isMaxLevel ? '已满级' : isLocked ? '未解锁' : !canAfford ? '银两不足' : '购买'}
</button>
</div> </div>
`; `;
}).join('')}</div>`; }).join('')}</div>`;
} }
function isShopItemLocked(item, state) { function isShopItemLocked(item, state) {
// Simple unlock check: if has unlockConditions and none met, it's locked
if (!item.unlockConditions || item.unlockConditions.length === 0) return false; if (!item.unlockConditions || item.unlockConditions.length === 0) return false;
// We can't easily call evaluateCondition from here without importing it,
// so we do a basic check for common conditions
for (const cond of item.unlockConditions) { for (const cond of item.unlockConditions) {
if (cond.type === 'age' && cond.op === '>=' && state.age < cond.value) return true; if (cond.type === 'age' && cond.op === '>=' && state.age < cond.value) return true;
if (cond.type === 'careerLevel') { if (cond.type === 'careerLevel') {
@@ -284,7 +300,6 @@ function isShopItemLocked(item, state) {
if (state.identity !== cond.value) return true; if (state.identity !== cond.value) return true;
} }
if (cond.type === 'or') { if (cond.type === 'or') {
// For OR conditions, if any sub-condition passes, it's unlocked
const subConditions = cond.conditions || []; const subConditions = cond.conditions || [];
const anyMet = subConditions.some(sub => !isShopItemLocked({ unlockConditions: [sub] }, state)); const anyMet = subConditions.some(sub => !isShopItemLocked({ unlockConditions: [sub] }, state));
if (!anyMet) return true; if (!anyMet) return true;
@@ -322,9 +337,14 @@ export function renderLogs(state) {
if (recentLogs.length === 0) { if (recentLogs.length === 0) {
els.logStream.innerHTML = '<div class="log-entry"><span class="log-time">[0年0天]</span><span class="log-normal">等待开始...</span></div>'; els.logStream.innerHTML = '<div class="log-entry"><span class="log-time">[0年0天]</span><span class="log-normal">等待开始...</span></div>';
lastLogCount = 0;
return; return;
} }
// 只在日志数量变化时更新 DOM,避免不必要的闪烁
if (recentLogs.length === lastLogCount) return;
lastLogCount = recentLogs.length;
els.logStream.innerHTML = recentLogs.map(log => { els.logStream.innerHTML = recentLogs.map(log => {
const typeClass = log.type === 'death' ? 'log-death' : const typeClass = log.type === 'death' ? 'log-death' :
log.type === 'major' ? 'log-major' : log.type === 'major' ? 'log-major' :
@@ -346,30 +366,77 @@ export function renderLogs(state) {
export function renderEventPanel(state) { export function renderEventPanel(state) {
const ev = state.currentEvent; const ev = state.currentEvent;
if (!ev) { if (!ev) {
if (els.eventTitle) els.eventTitle.textContent = ''; if (lastEventId !== null) {
if (els.eventText) els.eventText.textContent = ''; if (els.eventTitle) els.eventTitle.textContent = '';
if (els.eventTimer) els.eventTimer.textContent = ''; if (els.eventText) els.eventText.textContent = '';
if (els.eventChoices) els.eventChoices.innerHTML = ''; if (els.eventTimer) els.eventTimer.textContent = '';
if (els.eventChoices) els.eventChoices.innerHTML = '';
setEventBgImage(null);
lastEventId = null;
}
return; return;
} }
if (els.eventTitle) els.eventTitle.textContent = ev.title || ''; const evId = ev.id || ev.title || '';
if (els.eventText) els.eventText.textContent = ev.text || ''; // 只在事件变化时重建 DOM,避免高倍速下按钮被反复销毁导致无法点击
if (evId !== lastEventId) {
lastEventId = evId;
if (els.eventTitle) els.eventTitle.textContent = ev.title || '';
if (els.eventText) els.eventText.textContent = ev.text || '';
setEventBgImage(ev.image || null);
if (els.eventChoices) { if (els.eventChoices) {
if (ev.isChoice && Array.isArray(ev.choices)) { if (ev.isChoice && Array.isArray(ev.choices)) {
els.eventChoices.innerHTML = ev.choices.map((choice, idx) => { els.eventChoices.innerHTML = ev.choices.map((choice, idx) => {
const disabled = choice.requirement && !choice.requirement(state); const disabled = choice.requirements && !evaluateCondition(choice.requirements, state);
return `<button class="choice-btn" data-index="${idx}" ${disabled ? 'disabled' : ''}> return `<button class="choice-btn" data-index="${idx}" ${disabled ? 'disabled' : ''}>
${choice.text}${disabled ? ' (条件不足)' : ''} ${choice.text}${disabled ? '<span class="req-missing">(条件不足)</span>' : ''}
</button>`; </button>`;
}).join(''); }).join('');
} else { } else {
els.eventChoices.innerHTML = ''; els.eventChoices.innerHTML = '';
}
} }
} }
} }
export function setEventBgImage(imageUrl) {
if (!els.eventBgImage) return;
if (!imageUrl) {
els.eventBgImage.classList.remove('active');
els.eventBgImage.style.backgroundImage = '';
return;
}
els.eventBgImage.style.backgroundImage = `url('${imageUrl}')`;
els.eventBgImage.classList.add('active');
}
export function renderMemories(state) {
if (!els.memoryList) return;
const memories = state.memories || [];
if (memories.length === 0) {
els.memoryList.innerHTML = '<span class="empty-text">暂无记忆</span>';
return;
}
els.memoryList.innerHTML = memories.map(m => `<span class="memory-tag">${m}</span>`).join('');
}
export function renderMetaExp(state, careerConfig) {
if (!els.metaExpList) return;
const metaExp = state.metaExp || {};
const ids = Object.keys(metaExp).filter(k => metaExp[k] > 0);
if (ids.length === 0) {
els.metaExpList.innerHTML = '<span class="empty-text">尚无积累</span>';
return;
}
const careers = careerConfig?.careers || [];
els.metaExpList.innerHTML = ids.map(id => {
const cfg = careers.find(c => c.id === id);
const name = cfg?.name || id;
return `<div class="meta-exp-item">${name}: <span>+${Math.floor(metaExp[id])}</span> 元经验</div>`;
}).join('');
}
export function showDeathScreen(state, careerConfig) { export function showDeathScreen(state, careerConfig) {
if (!els.deathScreen || !els.deathContent) return; if (!els.deathScreen || !els.deathContent) return;
@@ -377,7 +444,6 @@ export function showDeathScreen(state, careerConfig) {
const age = state.age || 0; const age = state.age || 0;
const day = state.day || 0; const day = state.day || 0;
// Career achievements
const careers = state.careers || {}; const careers = state.careers || {};
const configCareers = careerConfig?.careers || []; const configCareers = careerConfig?.careers || [];
const careerAchievements = Object.entries(careers) const careerAchievements = Object.entries(careers)
@@ -390,8 +456,8 @@ export function showDeathScreen(state, careerConfig) {
els.deathContent.innerHTML = ` els.deathContent.innerHTML = `
<div class="death-summary"> <div class="death-summary">
<h2>身死道消</h2> <h2>身死道消</h2>
<p>死因:${reason}</p> <p>死因:<strong>${reason}</strong></p>
<p>享年:${age}${day % 365} 天</p> <p>享年:<strong>${age}${day % 365} 天</strong></p>
<h4>职业成就</h4> <h4>职业成就</h4>
<div class="career-achievements">${careerAchievements.length > 0 ? careerAchievements.join('<br>') : '碌碌无为'}</div> <div class="career-achievements">${careerAchievements.length > 0 ? careerAchievements.join('<br>') : '碌碌无为'}</div>
</div> </div>
@@ -406,6 +472,7 @@ export function hideChoiceEvent() {
if (els.eventText) els.eventText.textContent = ''; if (els.eventText) els.eventText.textContent = '';
if (els.eventTimer) els.eventTimer.textContent = ''; if (els.eventTimer) els.eventTimer.textContent = '';
if (els.eventChoices) els.eventChoices.innerHTML = ''; if (els.eventChoices) els.eventChoices.innerHTML = '';
setEventBgImage(null);
} }
export function hideDeathScreen() { export function hideDeathScreen() {
@@ -416,7 +483,7 @@ export function hideDeathScreen() {
export function updateTimer(seconds) { export function updateTimer(seconds) {
if (els.eventTimer) { if (els.eventTimer) {
els.eventTimer.textContent = seconds > 0 ? `${seconds}` : ''; els.eventTimer.textContent = seconds > 0 ? `${seconds}后自动选择` : '';
} }
} }
@@ -430,5 +497,18 @@ export function updatePauseButton(paused) {
const pauseBtn = document.getElementById('pause-btn'); const pauseBtn = document.getElementById('pause-btn');
if (pauseBtn) { if (pauseBtn) {
pauseBtn.textContent = paused ? '继续' : '暂停'; pauseBtn.textContent = paused ? '继续' : '暂停';
pauseBtn.classList.toggle('active', paused);
} }
} }
export function showToast(message) {
if (!els.toast) return;
els.toast.textContent = message;
els.toast.classList.add('show');
setTimeout(() => els.toast.classList.remove('show'), 2500);
}
export function hideTitleScreen() {
if (els.titleScreen) els.titleScreen.classList.add('hidden');
if (els.gameContainer) els.gameContainer.style.opacity = '1';
}
+41
View File
@@ -0,0 +1,41 @@
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium-browser',
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
page.on('console', msg => console.log('PAGE CONSOLE:', msg.text()));
page.on('pageerror', err => console.log('PAGE ERROR:', err.message));
await page.goto('http://localhost:8765', { waitUntil: 'networkidle0' });
// Click start button
await page.click('.choice-btn');
await page.waitForTimeout(500);
// Try to make a choice in child event
const buttons = await page.$$('.choice-btn');
console.log('Child event buttons:', buttons.length);
if (buttons.length > 0) {
await buttons[0].click();
await page.waitForTimeout(500);
}
// Check what happened
const title = await page.$eval('#eventTitle', el => el.textContent);
const age = await page.$eval('#age', el => el.textContent);
console.log('After first choice - Title:', title, 'Age:', age);
const buttons2 = await page.$$('.choice-btn');
console.log('Buttons after first choice:', buttons2.length);
if (buttons2.length > 0) {
const texts = await Promise.all(buttons2.map(b => b.evaluate(el => el.textContent)));
console.log('Button texts:', texts);
}
await browser.close();
})();
+159
View File
@@ -0,0 +1,159 @@
// ==================== 浏览器端到端测试 ====================
import puppeteer from 'puppeteer-core';
import http from 'http';
import fs from 'fs';
import path from 'path';
// 简单HTTP服务器
function startServer(port = 8765) {
const mimeTypes = {
'.html': 'text/html',
'.js': 'application/javascript',
'.json': 'application/json',
'.css': 'text/css',
};
const server = http.createServer((req, res) => {
let filePath = '.' + (req.url === '/' ? '/index.html' : req.url);
const ext = path.extname(filePath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
fs.readFile(filePath, (err, content) => {
if (err) {
res.writeHead(404);
res.end('Not found');
return;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
});
});
return new Promise((resolve) => {
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
resolve(server);
});
});
}
async function runBrowserTest() {
const server = await startServer(8765);
const browser = await puppeteer.launch({
headless: true,
executablePath: '/usr/bin/chromium-browser',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
const errors = [];
const logs = [];
page.on('pageerror', (err) => {
console.error('Page error:', err.message);
errors.push(err.message);
});
page.on('console', (msg) => {
const text = msg.text();
logs.push(text);
if (msg.type() === 'error') {
console.error('Console error:', text);
errors.push(text);
}
});
page.on('response', (response) => {
if (response.status() === 404) {
console.error('404 Not Found:', response.url());
}
});
try {
console.log('\n=== 浏览器端到端测试 ===');
// 加载页面
console.log('1. 加载页面...');
await page.goto('http://localhost:8765', { waitUntil: 'networkidle0', timeout: 10000 });
await new Promise(r => setTimeout(r, 500));
const realErrors = errors.filter(e => !e.includes('favicon.ico') && !e.includes('404'));
if (realErrors.length > 0) {
throw new Error(`页面加载错误: ${realErrors.join(', ')}`);
}
console.log(' 页面加载成功,无JS错误');
// 检查初始状态
console.log('2. 检查初始状态...');
const age = await page.$eval('#age', el => el.textContent);
const money = await page.$eval('#money', el => el.textContent);
console.log(` 年龄: ${age}, 银两: ${money}`);
// 等待一段时间让游戏运行
console.log('3. 等待游戏运行5秒...');
await new Promise(r => setTimeout(r, 5000));
// 检查时间是否推进
console.log('4. 检查时间推进...');
const dayAfter = await page.$eval('#day', el => el.textContent);
const yearAfter = await page.$eval('#year', el => el.textContent);
console.log(` 年: ${yearAfter}, 天: ${dayAfter}`);
if (parseInt(dayAfter) <= 0) {
throw new Error('时间没有推进');
}
console.log(' 时间正常推进');
// 检查职业是否解锁
console.log('5. 检查职业面板...');
const careerText = await page.$eval('#career-list', el => el.textContent);
console.log(` 职业面板: ${careerText.substring(0, 50)}...`);
// 检查属性面板(6个属性)
console.log('6. 检查6属性面板...');
const body = await page.$eval('#stat-body', el => el.textContent);
const destiny = await page.$eval('#stat-destiny', el => el.textContent);
const business = await page.$eval('#stat-business', el => el.textContent);
const intelligence = await page.$eval('#stat-intelligence', el => el.textContent);
console.log(` 体质: ${body}, 天命: ${destiny}, 经商: ${business}, 智力: ${intelligence}`);
// 检查商铺面板
console.log('7. 检查商铺面板...');
const shopExists = await page.$('#shop-panel') !== null;
console.log(` 商铺面板存在: ${shopExists}`);
// 检查神器面板
console.log('8. 检查神器面板...');
const artifactExists = await page.$('#artifact-list') !== null;
console.log(` 神器面板存在: ${artifactExists}`);
// 点击暂停
console.log('9. 测试暂停按钮...');
await page.click('#pause-btn');
await new Promise(r => setTimeout(r, 500));
const dayPaused = await page.$eval('#day', el => el.textContent);
await new Promise(r => setTimeout(r, 1000));
const dayAfterPause = await page.$eval('#day', el => el.textContent);
console.log(` 暂停前天: ${dayPaused}, 暂停后1秒: ${dayAfterPause}`);
// 点击速度按钮
console.log('10. 测试速度切换...');
await page.click('[data-speed="1"]');
await new Promise(r => setTimeout(r, 1000));
console.log(' 速度切换成功');
console.log('\n=== 浏览器测试全部通过 ===');
} catch (e) {
console.error('\n=== 浏览器测试失败 ===');
console.error(e.message);
process.exitCode = 1;
} finally {
await browser.close();
server.close();
}
}
runBrowserTest();
+235
View File
@@ -0,0 +1,235 @@
// ==================== 全流程生命周期测试 ====================
import { createInitialState, resetForRebirth } from '../src/engine/state.js';
import { setCareerConfig } from '../src/engine/careerEngine.js';
import { setEventConfig } from '../src/engine/eventEngine.js';
import { setShopConfig } from '../src/engine/shopEngine.js';
import { tick } from '../src/engine/tickLoop.js';
import { applyEffect } from '../src/engine/effectApplier.js';
import { processDeath } from '../src/engine/deathEngine.js';
import { evaluateCondition } from '../src/engine/conditionEvaluator.js';
import fs from 'fs';
function assert(cond, msg) { if (!cond) throw new Error(msg); }
// 加载配置
const careers = JSON.parse(fs.readFileSync('./src/config/careers.json', 'utf8'));
const events = JSON.parse(fs.readFileSync('./src/config/events.json', 'utf8'));
const shop = JSON.parse(fs.readFileSync('./src/config/shop.json', 'utf8'));
const identities = JSON.parse(fs.readFileSync('./src/config/identities.json', 'utf8'));
const talents = JSON.parse(fs.readFileSync('./src/config/talents.json', 'utf8'));
setCareerConfig(careers);
setEventConfig(events);
setShopConfig(shop);
function log(msg) { console.log(` ${msg}`); }
// 辅助函数:自动处理选择事件(测试中不暂停)
function autoResolveChoice(state) {
if (state.paused && state.currentEvent && state.currentEvent.choices) {
// 找到第一个满足条件的选择,否则选最后一个
let choice = state.currentEvent.choices.find(c => !c.requirements || evaluateCondition(c.requirements, state));
if (!choice) choice = state.currentEvent.choices[state.currentEvent.choices.length - 1];
if (choice && choice.effects) {
for (const effect of choice.effects) applyEffect(effect, state);
}
state.paused = false;
state.currentEvent = null;
}
}
// ====== 测试1:第1世基础流程 ======
console.log('\n=== Test 1: 第1世基础流程 ===');
{
const state = createInitialState();
state.identity = 'peasant';
state.stats = { body: 4, wisdom: 3, charm: 3, destiny: 3, business: 1, intelligence: 2 };
// 运行到10岁
for (let i = 0; i < 365 * 10; i++) {
tick(state, 1, careers, events, shop);
autoResolveChoice(state);
if (!state.alive) break;
}
log(`年龄: ${state.age}`);
log(`银两: ${Math.floor(state.money)}`);
log(`职业: ${Object.keys(state.careers).join(', ') || '无'}`);
assert(state.age >= 9, '应该活到接近10岁');
assert(Object.keys(state.careers).includes('student'), '应该解锁学童');
assert(state.money > 0, '应该有银两收入');
log('第1世基础流程 PASS');
}
// ====== 测试2:职业解锁链 ======
console.log('\n=== Test 2: 职业解锁链 ===');
{
const state = createInitialState();
state.age = 10;
state.stats = { body: 20, wisdom: 30, charm: 10, destiny: 5, business: 5, intelligence: 10 };
// 设置学童100级
state.careers.student = { level: 100, exp: 0 };
// tick一次检查解锁
tick(state, 1, careers, events, shop);
autoResolveChoice(state);
log(`解锁职业: ${Object.keys(state.careers).join(', ')}`);
assert(Object.keys(state.careers).includes('book_boy'), '学童100级应解锁书童');
// 设置学童250级 + 14岁
state.careers.student.level = 250;
state.age = 14;
tick(state, 1, careers, events, shop);
autoResolveChoice(state);
log(`解锁职业: ${Object.keys(state.careers).join(', ')}`);
assert(Object.keys(state.careers).includes('scholar'), '学童250级+14岁应解锁书生');
// 士兵解锁
state.stats.body = 15;
tick(state, 1, careers, events, shop);
autoResolveChoice(state);
assert(Object.keys(state.careers).includes('soldier'), '14岁+体质15应解锁士兵');
log('职业解锁链 PASS');
}
// ====== 测试3:银两系统 ======
console.log('\n=== Test 3: 银两系统 ===');
{
const state = createInitialState();
state.age = 10;
state.stats = { body: 10, wisdom: 10, charm: 10, destiny: 10, business: 50, intelligence: 10 };
state.careers.student = { level: 0, exp: 0 };
tick(state, 1, careers, events, shop);
autoResolveChoice(state);
const income = state.money;
log(`经商50时每日收入: ${income.toFixed(2)}`);
assert(income > 1, '经商50应增加收入(1.5倍)');
log('银两系统 PASS');
}
// ====== 测试4:经验系统 + 升级 ======
console.log('\n=== Test 4: 经验系统 + 升级 ===');
{
const state = createInitialState();
state.age = 10;
state.stats = { body: 10, wisdom: 50, charm: 10, destiny: 10, business: 10, intelligence: 10 };
state.careers.student = { level: 0, exp: 0 };
// 运行一段时间
for (let i = 0; i < 100; i++) {
tick(state, 1, careers, events, shop);
autoResolveChoice(state);
}
log(`学童等级: ${state.careers.student.level}`);
log(`学童经验: ${Math.floor(state.careers.student.exp)}`);
assert(state.careers.student.level > 0, '应该升级了');
log('经验系统 PASS');
}
// ====== 测试5:入侵事件触发 ======
console.log('\n=== Test 5: 入侵事件触发 ===');
{
const state = createInitialState();
state.age = 39;
state.stats = { body: 50, wisdom: 50, charm: 50, destiny: 50, business: 50, intelligence: 50 };
state.careers.soldier = { level: 100, exp: 0 };
// 在40岁生日时触发入侵
state.day = 364;
state.year = 39;
tick(state, 1, careers, events, shop);
log(`年龄: ${state.age}`);
log(`暂停状态: ${state.paused}`);
log(`当前事件: ${state.currentEvent?.id || '无'}`);
assert(state.age === 40, '应该刚满40岁');
assert(state.paused === true, '选择事件应暂停游戏');
assert(state.currentEvent?.id === 'invasion_military_40', '应触发军事入侵事件');
log('入侵事件触发 PASS');
}
// ====== 测试6:跨轮继承 ======
console.log('\n=== Test 6: 跨轮继承 ===');
{
const state = createInitialState();
state.identity = 'peasant';
state.stats = { body: 4, wisdom: 3, charm: 3, destiny: 3, business: 1, intelligence: 2 };
// 模拟一轮游戏,获得一些积累
state.metaExp = { student: 500 };
state.artifacts = { immortal_sword: 3 };
state.memories = ['wisdom_boost'];
state.unlockedEntries = new Set(['sword_bone']);
state.reincarnation = 0;
// 运行30年,然后手动触发死亡(测试重点是跨轮继承,不是等自然死亡)
for (let i = 0; i < 365 * 30; i++) {
tick(state, 1, careers, events, shop);
autoResolveChoice(state);
if (!state.alive) break;
}
// 手动死亡以测试结算
applyEffect({ type: 'die', reason: '测试死亡' }, state);
assert(state.alive === false, '应该已死亡');
// 死亡结算
processDeath(state, careers, talents);
log(`死亡前元经验: ${JSON.stringify(state.metaExp)}`);
// 重生
resetForRebirth(state);
log(`重生后轮次: ${state.reincarnation}`);
log(`重生后神器: ${JSON.stringify(state.artifacts)}`);
log(`重生后记忆: ${JSON.stringify(state.memories)}`);
log(`重生后元经验: ${JSON.stringify(state.metaExp)}`);
log(`重生后银两: ${state.money}`);
log(`重生后职业: ${JSON.stringify(state.careers)}`);
assert(state.reincarnation === 1, '轮次应为1');
assert(state.artifacts.immortal_sword === 3, '神器应保留');
assert(state.metaExp.student > 500, '元经验应增加');
assert(state.money === 0, '银两应重置');
assert(Object.keys(state.careers).length === 0, '职业应重置');
log('跨轮继承 PASS');
}
// ====== 测试7:商铺购买 ======
console.log('\n=== Test 7: 商铺购买 ======');
{
const state = createInitialState();
state.money = 1000;
state.age = 15;
state.stats = { body: 10, wisdom: 10, charm: 10, destiny: 10, business: 10, intelligence: 10 };
import('../src/engine/shopEngine.js').then(({ buyItem, canBuyItem }) => {
// 购买大力丸
const canBuy = canBuyItem(shop, state, 'power_pill');
assert(canBuy === true, '应该能买大力丸');
const result = buyItem(shop, state, 'power_pill');
assert(result === true, '购买应成功');
assert(state.stats.body === 15, '体质应+5');
assert(state.money === 950, '银两应-50');
log('商铺购买 PASS');
});
}
console.log('\n=== 所有全流程测试通过 ===\n');
+45
View File
@@ -0,0 +1,45 @@
const puppeteer = require('puppeteer-core');
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
(async () => {
const browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium-browser',
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
page.on('console', msg => console.log('CONSOLE:', msg.type(), msg.text()));
page.on('pageerror', err => console.log('PAGEERROR:', err.message));
await page.goto('http://localhost:8765', { waitUntil: 'networkidle0' });
await sleep(1500);
// Check internal state
const stateInfo = await page.evaluate(() => {
return {
day: window._state?.day,
alive: window._state?.alive,
paused: window._state?.paused,
reincarnation: window._state?.reincarnation,
};
});
console.log('Internal state:', stateInfo);
// Try to manually tick
await page.evaluate(() => {
if (window._tick) {
window._tick(10);
console.log('Manual tick executed, day now:', window._state?.day);
} else {
console.log('tick function not exposed');
}
});
await sleep(500);
const day = await page.$eval('#day', el => el.textContent).catch(() => 'N/A');
console.log('Day after manual tick:', day);
await browser.close();
})();