Update campus behavior
This commit is contained in:
parent
39682f14fe
commit
fc5db6496f
@ -171,5 +171,3 @@
|
||||
* **策略性:** 不只是堆数值,而是在“做学术”和“搞政治”之间做平衡。
|
||||
* **情感反馈:** 从初期的无助,到中期的纠结,再到后期的“屠龙少年终成恶龙”或“桃李满天下”的感动。
|
||||
|
||||
===
|
||||
|
||||
|
||||
179
docs/校园行为系统文档.md
Normal file
179
docs/校园行为系统文档.md
Normal file
@ -0,0 +1,179 @@
|
||||
# 校园行为系统文档
|
||||
|
||||
本文档描述校园行为系统相关代码与配置,
|
||||
覆盖 `scripts/Campus`、`scripts/Core`、`scripts/Models`
|
||||
以及 `resources/definitions` 下的所有相关文件。
|
||||
|
||||
## 1. 系统概览
|
||||
|
||||
- 行为系统基于“优先级决策 + 有限状态机”。
|
||||
- 决策优先级:Critical(危机) -> AssignedTask(指令任务)
|
||||
-> Needs(需求) -> Trait(特质偏好) -> Idle(闲置)。
|
||||
- 行为数值由 `campus_behavior.json` 驱动,
|
||||
动作效果按“每秒”变化。
|
||||
- 所有行为通过 `GD.Print` 输出到控制台。
|
||||
|
||||
## 2. 代码文件说明
|
||||
|
||||
### 2.1 `scripts/Campus`
|
||||
|
||||
- `scripts/Campus/CampusBehaviorConfig.cs`
|
||||
- 定义行为系统枚举与配置结构。
|
||||
- 提供 `CampusBehaviorConfig` JSON 加载。
|
||||
- 提供 `CampusLocationRegistry` 与 `CampusBehaviorWorld`。
|
||||
- 定义 `CampusTask` 与 `CampusAgentNeeds`。
|
||||
- `scripts/Campus/CampusBehaviorAgent.cs`
|
||||
- 定义 `CampusAgentRuntime`、`CampusBehaviorIntent`、`CampusBehaviorContext`。
|
||||
- 实现 Planner 与多级 Provider。
|
||||
- 实现状态机:Decision/Move/Action。
|
||||
- `CampusBehaviorAgent` 负责每帧驱动与打断。
|
||||
- `CampusTraitIds` 提供行为特质 Id 常量。
|
||||
|
||||
### 2.2 `scripts/Core`
|
||||
|
||||
- `scripts/Core/ContentCollectionResource.cs`
|
||||
- 资源集合容器,可把多个定义资源打包到 `.tres`。
|
||||
- `scripts/Core/ContentRegistry.cs`
|
||||
- 内容加载与合并入口,支持 Resource/JSON 源。
|
||||
- `scripts/Core/ContentResources.cs`
|
||||
- 内容资源接口:`IContentResource`/`IContentResourceCollection`。
|
||||
- `scripts/Core/DisciplineDefinitionResource.cs`
|
||||
- 学科定义 `.tres` 到 `DisciplineDefinition` 的映射。
|
||||
- `scripts/Core/DomainEvents.cs`
|
||||
- 领域事件:任务完成/失败、回合结束。
|
||||
- `scripts/Core/EventBus.cs`
|
||||
- 轻量事件总线 `DomainEventBus`。
|
||||
- `scripts/Core/GameController.cs`
|
||||
- 回合阶段切换控制器。
|
||||
- `scripts/Core/GameSession.cs`
|
||||
- 会话入口,聚合 State/Content/Systems/Events/Localization。
|
||||
- `CreateDefault()` 负责加载 `.tres` 与 JSON。
|
||||
- `scripts/Core/GameSystems.cs`
|
||||
- 系统集合:Turn/Task/Economy/Synergy/Assignment。
|
||||
- `scripts/Core/LocalizationService.cs`
|
||||
- 多语言接口与 Godot 实现。
|
||||
- `scripts/Core/ModManifest.cs`
|
||||
- Mod 清单与包信息结构。
|
||||
- `scripts/Core/Mvc.cs`
|
||||
- MVC 接口与 `ModelView<TModel>` 基类。
|
||||
- `scripts/Core/StatResolver.cs`
|
||||
- 数值解析器(羁绊/学科/特质/装备叠加)。
|
||||
|
||||
### 2.3 `scripts/Models`
|
||||
|
||||
- `scripts/Models/CoreIds.cs`
|
||||
- 核心内容 Id 常量(学科/羁绊/角色等)。
|
||||
- `scripts/Models/DefinitionSupport.cs`
|
||||
- 通用定义结构:`LocalizedText` 与 `DefinitionHeader`。
|
||||
- `scripts/Models/DisciplineDefinitions.cs`
|
||||
- 学科定义 `DisciplineDefinition` 与 `DisciplineBuff`。
|
||||
- `scripts/Models/DomainEnums.cs`
|
||||
- 统一枚举:`AttributeType`、`ResourceType`、`StatusType`。
|
||||
- `scripts/Models/GameContentDatabase.cs`
|
||||
- 定义数据汇总库(学科/羁绊/任务/装备等)。
|
||||
- `scripts/Models/GameState.cs`
|
||||
- 全局运行时状态:回合/经济/人员/任务/库存/羁绊/肉鸽。
|
||||
- `scripts/Models/ItemDefinitions.cs`
|
||||
- 装备/设施/消耗品定义与分类枚举。
|
||||
- `scripts/Models/MentorModel.cs`
|
||||
- 导师模型与资源组件(能量)。
|
||||
- `scripts/Models/Modifiers.cs`
|
||||
- 通用数值修饰结构(属性/状态/资源 + RuleIds)。
|
||||
- `scripts/Models/PaperDefinitions.cs`
|
||||
- 论文卡牌定义(等级与元数据)。
|
||||
- `scripts/Models/PropertyValue.cs`
|
||||
- 通用数值类型,带范围与运算符重载。
|
||||
- `scripts/Models/RogueliteDefinitions.cs`
|
||||
- 肉鸽继承定义(校友录/传承/职称保留)。
|
||||
- `scripts/Models/StaffModel.cs`
|
||||
- 雇员/合作者模型(博后/小老师)与动机组件。
|
||||
- `scripts/Models/StatusValue.cs`
|
||||
- 带阈值的状态值(压力/理智/忠诚等)。
|
||||
- `scripts/Models/StudentModel.cs`
|
||||
- 学生模型(类型、进度、贡献记录)。
|
||||
- `scripts/Models/SynergyDefinitions.cs`
|
||||
- 羁绊/职业/特质定义与叠层结构 `SynergyTier`。
|
||||
- `scripts/Models/Task.cs`
|
||||
- 运行时任务模型 `TaskModel` 与进度数据结构。
|
||||
- `scripts/Models/TaskDefinitions.cs`
|
||||
- 任务定义与枚举(类型/难度/奖励/需求)。
|
||||
- `scripts/Models/UnitComponents.cs`
|
||||
- Unit 组件结构(身份/属性/状态/标签/分配/位置/装备)。
|
||||
- `scripts/Models/UnitModel.cs`
|
||||
- Unit 组合容器,聚合上述组件。
|
||||
|
||||
## 3. 配置文件说明(`resources/definitions`)
|
||||
|
||||
### 3.1 `resources/definitions/campus_behavior.json`
|
||||
|
||||
行为系统核心数值配置,动作数值按秒生效。
|
||||
|
||||
全局阈值与衰减:
|
||||
|
||||
- `CriticalSanityThreshold`:理智过低触发人工湖。
|
||||
- `CriticalStaminaThreshold`:体力过低触发强制休息。
|
||||
- `CriticalStressThreshold`:压力过高触发人工湖。
|
||||
- `HungerThreshold`:饥饿过低触发食堂。
|
||||
- `EnergyThreshold`:精力过低触发睡觉/咖啡。
|
||||
- `SocialThreshold`:社交过低触发社交行为。
|
||||
- `LowMoodThreshold`:心情过低触发宿舍休息。
|
||||
- `HungerDecayPerSecond`:基础饥饿衰减。
|
||||
- `EnergyDecayPerSecond`:基础精力衰减。
|
||||
- `StaminaDecayPerSecond`:基础体力衰减。
|
||||
- `StressGrowthPerSecond`:基础压力增长。
|
||||
- `SocialDecayPerSecond`:基础社交衰减。
|
||||
- `DecisionIntervalSeconds`:打断/重评估间隔。
|
||||
|
||||
ActionConfigs:
|
||||
|
||||
- `ActionId`:动作枚举名。
|
||||
- `LocationId`:地点枚举名。
|
||||
- `DurationSeconds`:动作持续时间。
|
||||
- `HungerDelta`/`EnergyDelta`/`StaminaDelta`。
|
||||
- `StressDelta`/`MoodDelta`/`SocialDelta`。
|
||||
- `SanityDelta`/`HealthDelta`。
|
||||
|
||||
### 3.2 `resources/definitions/archetypes.json`
|
||||
|
||||
- 数组形式的 `ArchetypeDefinition`。
|
||||
- `Header`:`Id`、`Name`、`Description`、`Tags`。
|
||||
- `Tiers`:叠层配置列表。
|
||||
- `RequiredCount`:触发层数。
|
||||
- `Modifiers`:数值修饰与 `RuleIds`。
|
||||
|
||||
### 3.3 `resources/definitions/roles.json`
|
||||
|
||||
- 数组形式的 `RoleDefinition`。
|
||||
- `Header`、`Tiers` 与 archetypes 一致。
|
||||
- `AllowedDisciplineIds`:学科限制列表。
|
||||
|
||||
### 3.4 `resources/definitions/traits.json`
|
||||
|
||||
- 数组形式的 `TraitDefinition`。
|
||||
- `Header`:Id 与名称说明。
|
||||
- `Modifiers`:数值修饰与 `RuleIds`。
|
||||
|
||||
### 3.5 `resources/definitions/disciplines.json`
|
||||
|
||||
- 数组形式的 `DisciplineDefinition`。
|
||||
- `Header`:学科 Id、名称、描述、Tags。
|
||||
- `Buff`:学科被动(Name/Description/Modifiers)。
|
||||
- `RolePoolIds`:角色池 Id 列表。
|
||||
- `ItemPoolIds`:装备池 Id 列表。
|
||||
- `TaskKeywordIds`:任务关键词 Id 列表。
|
||||
|
||||
### 3.6 `resources/definitions/discipline_biology.tres`
|
||||
|
||||
- Godot 资源版学科定义(`DisciplineDefinitionResource`)。
|
||||
- 主要字段:
|
||||
- `Id`/`NameKey`/`NameFallback`/`DescriptionKey`/`DescriptionFallback`。
|
||||
- `IconPath`/`Tags`。
|
||||
- `BuffNameKey`/`BuffNameFallback`。
|
||||
- `BuffDescriptionKey`/`BuffDescriptionFallback`/`BuffRuleIds`。
|
||||
- `RolePoolIds`/`ItemPoolIds`/`TaskKeywordIds`。
|
||||
|
||||
## 4. 规则扩展建议
|
||||
|
||||
- 新增地点:扩展 `CampusLocationId`,并添加 `Locations` 标记。
|
||||
- 新增动作:扩展 `CampusActionId`,并添加 ActionConfig。
|
||||
- 新增 AI 规则:新增 Provider 并加入 `BuildProviders()`。
|
||||
132
docs/角色与行为规则.md
Normal file
132
docs/角色与行为规则.md
Normal file
@ -0,0 +1,132 @@
|
||||
# 校园角色行为规则文档
|
||||
|
||||
**版本:** 1.0
|
||||
**依据:** 基于 `design.md`, `任务与经济系统.md`, `学科与流派系统.md`, `装备与设施系统.md`, `角色与羁绊系统.md` 整合生成。
|
||||
|
||||
---
|
||||
|
||||
## 1. 行为逻辑总述 (General Behavior Logic)
|
||||
|
||||
在游戏的“自走阶段”(30秒自由行动),角色(学生/导师/职工)并非仅仅是执行任务的机器,而是拥有**生理需求**、**心理状态**和**个人性格**的自主智能体。
|
||||
|
||||
角色的行为决策遵循以下**优先级队列**:
|
||||
|
||||
1. **生存危机 (Critical State):** 压力爆表(San值过低)或 体力耗尽 $\rightarrow$ 强制中断当前行为,前往特定场所恢复。
|
||||
2. **导师指令 (Assigned Task):** 玩家手动拖拽或分配的任务 $\rightarrow$ 前往对应的工作场所(实验室/图书馆/机房)。
|
||||
3. **生理/心理需求 (Needs):** 饥饿、轻度疲劳、需要社交 $\rightarrow$ 前往食堂、寝室或咖啡店。
|
||||
4. **特质驱动 (Trait-Driven):** 基于“卷王”、“摸鱼党”等标签的自发行为 $\rightarrow$ 加班或闲逛。
|
||||
5. **闲置/游荡 (Idle):** 无事可做时在校园内随机移动或触发彩蛋交互。
|
||||
|
||||
---
|
||||
|
||||
## 2. 场所行为详解 (Location Behaviors)
|
||||
|
||||
校园分为8个核心区域,每个区域对应特定的行为模式和属性变化。
|
||||
|
||||
### 2.1 实验室 (Laboratory)
|
||||
* **功能定义:** 科研生产的核心区域,进行“学术探索”任务。
|
||||
* **进入条件:**
|
||||
* 被分配了【实验类】任务(如“产出论文草稿”)。
|
||||
* 具有【卷王】特质的角色在闲置时会自动进入。
|
||||
* 具有【炼金术士】、【实验狗】标签的角色偏好此地。
|
||||
* **行为状态:**
|
||||
* **做实验 (Experimenting):** 消耗体力,增加压力,产出任务进度。
|
||||
* **仪器操作 (Operating):** 只有装备了特定道具(如移液枪)或特定职业角色才会触发的高效动作。
|
||||
* **特殊事件:**
|
||||
* *爆炸:* 化学/生化类角色有概率触发,导致周围角色停止动作并扣血。
|
||||
* *甚至不是人(AI成精):* 不进入实验室,而是通过网络远程占用设备。
|
||||
|
||||
### 2.2 图书馆 (Library)
|
||||
* **功能定义:** 知识获取与文本生产区域。
|
||||
* **进入条件:**
|
||||
* 被分配了【写作类】任务(如“论文润色”、“文献综述”)。
|
||||
* 具有【笔杆子】、【思想者】、【写手】标签的角色偏好此地。
|
||||
* 角色处于“查阅资料”状态(任务进度卡住时触发)。
|
||||
* **行为状态:**
|
||||
* **静默写作 (Silent Writing):** 持续产出,低噪音。
|
||||
* **寻找灵感 (Seeking Inspiration):** 在书架间移动,不产出进度,但增加下一次产出的暴击率。
|
||||
|
||||
### 2.3 食堂 (Canteen)
|
||||
* **功能定义:** 生理补给站。
|
||||
* **进入条件:**
|
||||
* 角色【饥饿值】低于阈值。
|
||||
* 【大胃王】特质的角色会频繁进入。
|
||||
* 午餐/晚餐时间(游戏内特定时间段)引发群体移动。
|
||||
* **行为状态:**
|
||||
* **进食 (Eating):** 消耗金钱(玩家资金),快速恢复体力。
|
||||
* **吐槽 (Gossiping):** 进食时与邻座角色交互,随机恢复心情或传播“摸鱼”Buff。
|
||||
|
||||
### 2.4 寝室 (Dormitory)
|
||||
* **功能定义:** 休息与娱乐,San值避风港。
|
||||
* **进入条件:**
|
||||
* 角色【体力】极低或【心情】较差。
|
||||
* 具有【摸鱼党】特质的角色在工作间隙溜入。
|
||||
* 【极客】职业的角色可能在寝室进行“远程办公”(如果有相关羁绊)。
|
||||
* **行为状态:**
|
||||
* **睡眠 (Sleeping):** 大幅恢复体力,时间较长。
|
||||
* **打游戏/刷剧 (Gaming/Chilling):** 恢复心情,恢复少量体力。
|
||||
* **甚至不回寝室:** 【卷王】(6层羁绊) 会无视寝室需求,直接在工位打地铺。
|
||||
|
||||
### 2.5 人工湖 (Artificial Lake)
|
||||
* **功能定义:** 心理急救中心。
|
||||
* **进入条件:**
|
||||
* 角色处于**【崩溃 (Mental Breakdown)】**状态(压力满)。
|
||||
* 角色处于**【顿悟 (Epiphany)】**前摇状态(常见于【思想者】职业)。
|
||||
* **行为状态:**
|
||||
* **发呆/看鸭子 (Staring):** 角色停止一切响应,缓慢降低压力值。若被打断,压力值回弹。
|
||||
* **跳河 (Suicide):** 极低概率触发,因为长期处于崩溃状态导致角色退出。
|
||||
|
||||
### 2.6 咖啡店 (Coffee Shop)
|
||||
* **功能定义:** 兴奋剂补给站,Buff获取点。
|
||||
* **进入条件:**
|
||||
* 角色【精力】下降但仍有任务在身。
|
||||
* 具有【咖啡因依赖】特质的角色必须定期进入,否则通过Debuff降低效率。
|
||||
* 【富家子弟】偏好的休息场所。
|
||||
* **行为状态:**
|
||||
* **购买饮料 (Buying):** 消耗金钱,获得短时“攻速提升”Buff。
|
||||
* **商务洽谈 (Business Talk):** 【大忽悠】或【经济学】角色在此可能触发“获得额外经费”的小事件。
|
||||
|
||||
### 2.7 行政楼 (Administration Building)
|
||||
* **功能定义:** 财务报销与行政手续处理。
|
||||
* **进入条件:**
|
||||
* 任务阶段涉及到“报销”、“审批”环节。
|
||||
* 触发【事务型任务】(如“年度报表”、“安全检查”)。
|
||||
* 【管家】、【调研员】常驻区域。
|
||||
* **行为状态:**
|
||||
* **排队 (Queuing):** 极度消耗心情,无产出。
|
||||
* **扯皮 (Arguing):** 【刺头】或【法学】角色可能触发,减少排队时间或免除惩罚。
|
||||
* **盖章 (Stamping):** 任务完成的必要步骤。
|
||||
|
||||
### 2.8 足球场 (Football Field)
|
||||
* **功能定义:** 锻炼身体,发泄精力。
|
||||
* **进入条件:**
|
||||
* 角色【健康值】较低,需要锻炼。
|
||||
* 【环境科学】、【调研员】等高体力需求职业的训练场。
|
||||
* 没有任务且不想回寝室时。
|
||||
* **行为状态:**
|
||||
* **跑步 (Running):** 恢复健康值,微量降低压力。
|
||||
* **甚至在踢球:** 如果凑齐足够多的【摸鱼党】,可能会组织一场球赛,吸引周围人围观(降低周围人工作效率)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 角色特质对行为的修正 (Trait Modifiers)
|
||||
|
||||
角色的特质(Traits)和羁绊(Synergies)会覆盖或修正上述默认行为规则。
|
||||
|
||||
| 特质/羁绊 | 修正规则 |
|
||||
| :--- | :--- |
|
||||
| **【卷王】 (The Grinder)** | **拒绝休息:** 当体力/心情低于阈值时,仍有 50% 概率拒绝前往寝室/湖边,而是去咖啡店买咖啡强撑。 |
|
||||
| **【摸鱼党】 (The Slacker)** | **路径偏移:** 在前往实验室/图书馆的路上,有 30% 概率“迷路”进食堂或寝室逗留 5 秒。 |
|
||||
| **【社恐】 (Social Phobia)** | **避开人群:** 动态检测人数。如果目标区域(如食堂)人数 > 3,会转身去买自动贩卖机或饿着,或者去无人的角落(如人工湖死角)。 |
|
||||
| **【社牛/大忽悠】** | **聚集效应:** 倾向于前往人数最多的区域,且进入后会引发周围角色短暂停顿(听他说话)。 |
|
||||
| **【洁癖/强迫症】** | **环境敏感:** 如果实验室发生过“爆炸”或有人呕吐,会拒绝进入该区域直到被清理。 |
|
||||
| **【甚至不是人】** | **幽灵行为:** 不需要进食(不去食堂),不需要睡觉(不去寝室),只消耗电费(常驻机房/实验室)。 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 寻路与交互逻辑 (Pathfinding & Interaction)
|
||||
|
||||
* **碰撞体积:** 角色之间存在碰撞体积,狭窄通道(如行政楼走廊)容易发生拥堵,增加焦躁感(压力上升)。
|
||||
* **交互气泡:**
|
||||
* 角色头顶会显示当前意图的气泡图标(如:🍔 = 去食堂,🧪 = 去实验室,💤 = 去寝室)。
|
||||
* 当两个角色相遇时,若羁绊相关(如两个【二次元】),会弹出“握手”或“爱心”表情,并短暂交换Buff。
|
||||
21
package-lock.json
generated
Normal file
21
package-lock.json
generated
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "super-mentor",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"yargs-parser": "^22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"yargs-parser": "^22.0.0"
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@
|
||||
},
|
||||
"Description": {
|
||||
"Key": "archetype.grinder.desc",
|
||||
"Fallback": "They live in the lab and push everything faster."
|
||||
"Fallback": "Obsessed with work; always pushing the lab faster."
|
||||
},
|
||||
"Tags": [ "archetype" ]
|
||||
},
|
||||
@ -17,18 +17,27 @@
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Activation", "Add": 5, "Multiplier": 1.1 }
|
||||
{ "Type": "Activation", "Add": 0, "Multiplier": 1.15 }
|
||||
],
|
||||
"RuleIds": [ "rule:grinder_stress_up" ]
|
||||
"RuleIds": [ "rule:grinder_stress_growth_20" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Activation", "Add": 10, "Multiplier": 1.2 }
|
||||
{ "Type": "Activation", "Add": 0, "Multiplier": 1.35 }
|
||||
],
|
||||
"RuleIds": [ "rule:grinder_overwork" ]
|
||||
"RuleIds": [ "rule:grinder_overwork_rage" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Activation", "Add": 0, "Multiplier": 1.60 }
|
||||
],
|
||||
"RuleIds": [ "rule:grinder_no_sleep_life_cost" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -42,7 +51,7 @@
|
||||
},
|
||||
"Description": {
|
||||
"Key": "archetype.slacker.desc",
|
||||
"Fallback": "They recover morale but slow down the team."
|
||||
"Fallback": "Finds the best corners to rest and spread chill vibes."
|
||||
},
|
||||
"Tags": [ "archetype" ]
|
||||
},
|
||||
@ -51,18 +60,129 @@
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"StatusModifiers": [
|
||||
{ "Type": "Mood", "Add": 5, "Multiplier": 1.05 }
|
||||
{ "Type": "Mood", "Add": 0, "Multiplier": 1.50 }
|
||||
],
|
||||
"RuleIds": [ "rule:slacker_relax" ]
|
||||
"RuleIds": [ "rule:slacker_rest_recovery_50" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"StatusModifiers": [
|
||||
{ "Type": "Stress", "Add": -5, "Multiplier": 0.9 }
|
||||
{ "Type": "Stress", "Add": 0, "Multiplier": 1.30 }
|
||||
],
|
||||
"RuleIds": [ "rule:slacker_spread" ]
|
||||
"RuleIds": [ "rule:slacker_spread_breaks" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:slacker_no_reputation_loss_on_fail" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:archetype_elite",
|
||||
"Name": {
|
||||
"Key": "archetype.elite.name",
|
||||
"Fallback": "Elite"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "archetype.elite.desc",
|
||||
"Fallback": "Well-funded and well-connected; money solves problems."
|
||||
},
|
||||
"Tags": [ "archetype" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"ResourceModifiers": [
|
||||
{ "Type": "Money", "Add": 0, "Multiplier": 1.10 }
|
||||
],
|
||||
"RuleIds": [ "rule:elite_interest_10" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:elite_shop_tier_up", "rule:elite_refund_50" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:elite_buy_progress" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:archetype_prodigy",
|
||||
"Name": {
|
||||
"Key": "archetype.prodigy.name",
|
||||
"Fallback": "Prodigy"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "archetype.prodigy.desc",
|
||||
"Fallback": "Brilliant but eccentric; thrives in extremes."
|
||||
},
|
||||
"Tags": [ "archetype" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Academic", "Add": 0, "Multiplier": 1.10 }
|
||||
],
|
||||
"RuleIds": [ "rule:prodigy_crit_15" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:prodigy_crit_50_on_s_tasks" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:prodigy_solitary_scaling" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:archetype_mascot",
|
||||
"Name": {
|
||||
"Key": "archetype.mascot.name",
|
||||
"Fallback": "Mascot"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "archetype.mascot.desc",
|
||||
"Fallback": "Low output, high morale and administrative favors."
|
||||
},
|
||||
"Tags": [ "archetype" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"StatusModifiers": [
|
||||
{ "Type": "Mood", "Add": 2, "Multiplier": 1.0 }
|
||||
],
|
||||
"RuleIds": [ "rule:mascot_mood_regen" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:mascot_admin_fast", "rule:mascot_admin_immunity" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
140
resources/definitions/campus_behavior.json
Normal file
140
resources/definitions/campus_behavior.json
Normal file
@ -0,0 +1,140 @@
|
||||
{
|
||||
"CriticalSanityThreshold": 15,
|
||||
"CriticalStaminaThreshold": 12,
|
||||
"CriticalStressThreshold": 90,
|
||||
"HungerThreshold": 30,
|
||||
"EnergyThreshold": 25,
|
||||
"SocialThreshold": 35,
|
||||
"LowMoodThreshold": 25,
|
||||
"HungerDecayPerSecond": 0.60,
|
||||
"EnergyDecayPerSecond": 0.50,
|
||||
"StaminaDecayPerSecond": 0.40,
|
||||
"StressGrowthPerSecond": 0.45,
|
||||
"SocialDecayPerSecond": 0.35,
|
||||
"DecisionIntervalSeconds": 0.50,
|
||||
"ActionConfigs": [
|
||||
{
|
||||
"ActionId": "Experimenting",
|
||||
"LocationId": "Laboratory",
|
||||
"DurationSeconds": 6.0,
|
||||
"HungerDelta": -0.60,
|
||||
"EnergyDelta": -0.80,
|
||||
"StaminaDelta": -1.00,
|
||||
"StressDelta": 0.90,
|
||||
"MoodDelta": -0.20,
|
||||
"SocialDelta": -0.10
|
||||
},
|
||||
{
|
||||
"ActionId": "Writing",
|
||||
"LocationId": "Library",
|
||||
"DurationSeconds": 6.0,
|
||||
"HungerDelta": -0.40,
|
||||
"EnergyDelta": -0.60,
|
||||
"StaminaDelta": -0.60,
|
||||
"StressDelta": 0.60,
|
||||
"MoodDelta": -0.10,
|
||||
"SocialDelta": -0.20
|
||||
},
|
||||
{
|
||||
"ActionId": "Eating",
|
||||
"LocationId": "Canteen",
|
||||
"DurationSeconds": 4.5,
|
||||
"HungerDelta": 3.00,
|
||||
"EnergyDelta": 0.40,
|
||||
"StaminaDelta": 0.80,
|
||||
"StressDelta": -0.50,
|
||||
"MoodDelta": 0.30,
|
||||
"SocialDelta": 0.40
|
||||
},
|
||||
{
|
||||
"ActionId": "Sleeping",
|
||||
"LocationId": "Dormitory",
|
||||
"DurationSeconds": 7.5,
|
||||
"HungerDelta": -0.20,
|
||||
"EnergyDelta": 2.40,
|
||||
"StaminaDelta": 2.20,
|
||||
"StressDelta": -0.90,
|
||||
"MoodDelta": 0.50,
|
||||
"SocialDelta": -0.20
|
||||
},
|
||||
{
|
||||
"ActionId": "Chilling",
|
||||
"LocationId": "Dormitory",
|
||||
"DurationSeconds": 5.0,
|
||||
"HungerDelta": -0.10,
|
||||
"EnergyDelta": 0.80,
|
||||
"StaminaDelta": 0.60,
|
||||
"StressDelta": -0.50,
|
||||
"MoodDelta": 0.80,
|
||||
"SocialDelta": 0.10
|
||||
},
|
||||
{
|
||||
"ActionId": "Staring",
|
||||
"LocationId": "ArtificialLake",
|
||||
"DurationSeconds": 5.5,
|
||||
"HungerDelta": -0.20,
|
||||
"EnergyDelta": 0.20,
|
||||
"StaminaDelta": 0.20,
|
||||
"StressDelta": -1.20,
|
||||
"MoodDelta": 0.30,
|
||||
"SocialDelta": -0.30,
|
||||
"SanityDelta": 0.80
|
||||
},
|
||||
{
|
||||
"ActionId": "CoffeeBreak",
|
||||
"LocationId": "CoffeeShop",
|
||||
"DurationSeconds": 4.0,
|
||||
"HungerDelta": -0.10,
|
||||
"EnergyDelta": 1.50,
|
||||
"StaminaDelta": 0.30,
|
||||
"StressDelta": -0.20,
|
||||
"MoodDelta": 0.40,
|
||||
"SocialDelta": 0.30
|
||||
},
|
||||
{
|
||||
"ActionId": "Administration",
|
||||
"LocationId": "AdministrationBuilding",
|
||||
"DurationSeconds": 5.0,
|
||||
"HungerDelta": -0.20,
|
||||
"EnergyDelta": -0.40,
|
||||
"StaminaDelta": -0.30,
|
||||
"StressDelta": 0.80,
|
||||
"MoodDelta": -0.60,
|
||||
"SocialDelta": -0.10
|
||||
},
|
||||
{
|
||||
"ActionId": "Running",
|
||||
"LocationId": "FootballField",
|
||||
"DurationSeconds": 4.5,
|
||||
"HungerDelta": -0.30,
|
||||
"EnergyDelta": -0.20,
|
||||
"StaminaDelta": -0.20,
|
||||
"StressDelta": -0.40,
|
||||
"MoodDelta": 0.40,
|
||||
"SocialDelta": 0.20,
|
||||
"HealthDelta": 0.80
|
||||
},
|
||||
{
|
||||
"ActionId": "Socializing",
|
||||
"LocationId": "CoffeeShop",
|
||||
"DurationSeconds": 4.5,
|
||||
"HungerDelta": -0.10,
|
||||
"EnergyDelta": 0.30,
|
||||
"StaminaDelta": 0.10,
|
||||
"StressDelta": -0.30,
|
||||
"MoodDelta": 0.70,
|
||||
"SocialDelta": 1.20
|
||||
},
|
||||
{
|
||||
"ActionId": "Wandering",
|
||||
"LocationId": "RandomWander",
|
||||
"DurationSeconds": 4.0,
|
||||
"HungerDelta": -0.10,
|
||||
"EnergyDelta": -0.10,
|
||||
"StaminaDelta": -0.10,
|
||||
"StressDelta": -0.10,
|
||||
"MoodDelta": 0.10,
|
||||
"SocialDelta": -0.10
|
||||
}
|
||||
]
|
||||
}
|
||||
438
resources/definitions/roles.json
Normal file
438
resources/definitions/roles.json
Normal file
@ -0,0 +1,438 @@
|
||||
[
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_coder",
|
||||
"Name": {
|
||||
"Key": "role.coder.name",
|
||||
"Fallback": "Coder"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.coder.desc",
|
||||
"Fallback": "Specializes in engineering tasks and computer rooms."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Engineering", "Add": 0, "Multiplier": 1.20 }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:coder_copy_paste" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:coder_remote_work" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_writer",
|
||||
"Name": {
|
||||
"Key": "role.writer.name",
|
||||
"Fallback": "Writer"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.writer.desc",
|
||||
"Fallback": "Turns ideas into papers with steady output."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Writing", "Add": 0, "Multiplier": 1.20 }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:writer_citation_bonus" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:writer_auto_generation" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_lab_rat",
|
||||
"Name": {
|
||||
"Key": "role.lab_rat.name",
|
||||
"Fallback": "Lab Rat"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.lab_rat.desc",
|
||||
"Fallback": "Lives in the lab and keeps experiments running."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Activation", "Add": 0, "Multiplier": 1.10 }
|
||||
],
|
||||
"RuleIds": [ "rule:lab_rat_move_speed_30" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"ResourceModifiers": [
|
||||
{ "Type": "Money", "Add": 0, "Multiplier": 1.40 }
|
||||
],
|
||||
"RuleIds": [ "rule:lab_rat_extra_reagents" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:lab_rat_dual_equipment" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_presenter",
|
||||
"Name": {
|
||||
"Key": "role.presenter.name",
|
||||
"Fallback": "Presenter"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.presenter.desc",
|
||||
"Fallback": "Handles defenses, meetings, and funding pitches."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Social", "Add": 0, "Multiplier": 1.20 }
|
||||
],
|
||||
"RuleIds": [ "rule:presenter_pitch_success_20" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:presenter_double_funding" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:presenter_hype_aura" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_scribe",
|
||||
"Name": {
|
||||
"Key": "role.scribe.name",
|
||||
"Fallback": "Scribe"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.scribe.desc",
|
||||
"Fallback": "A reliable writing role used by all disciplines."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Writing", "Add": 0, "Multiplier": 1.15 }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:scribe_reputation_bonus" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:scribe_split_publication" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_orator",
|
||||
"Name": {
|
||||
"Key": "role.orator.name",
|
||||
"Fallback": "Orator"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.orator.desc",
|
||||
"Fallback": "Brings in resources and keeps morale afloat."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Social", "Add": 0, "Multiplier": 1.15 }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"ResourceModifiers": [
|
||||
{ "Type": "Money", "Add": 0, "Multiplier": 1.30 }
|
||||
],
|
||||
"RuleIds": [ "rule:orator_extra_funding" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:orator_morale_shield" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_steward",
|
||||
"Name": {
|
||||
"Key": "role.steward.name",
|
||||
"Fallback": "Steward"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.steward.desc",
|
||||
"Fallback": "Keeps admin tasks and logistics under control."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Activation", "Add": 0, "Multiplier": 1.10 }
|
||||
],
|
||||
"RuleIds": [ "rule:steward_admin_speed_50" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"ResourceModifiers": [
|
||||
{ "Type": "Money", "Add": 0, "Multiplier": 0.80 }
|
||||
],
|
||||
"RuleIds": [ "rule:steward_cost_reduction" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:steward_auto_supply" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_alchemist",
|
||||
"Name": {
|
||||
"Key": "role.alchemist.name",
|
||||
"Fallback": "Alchemist"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.alchemist.desc",
|
||||
"Fallback": "Consumes reagents for high-risk experiments."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"ResourceModifiers": [
|
||||
{ "Type": "Money", "Add": 0, "Multiplier": 0.80 }
|
||||
],
|
||||
"RuleIds": [ "rule:alchemist_reagent_discount" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:alchemist_explosion_5", "rule:alchemist_success_boost" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:alchemist_fail_progress_persist" ]
|
||||
}
|
||||
}
|
||||
],
|
||||
"AllowedDisciplineIds": [
|
||||
"core:discipline_biology",
|
||||
"core:discipline_chemistry",
|
||||
"core:discipline_environment",
|
||||
"core:discipline_materials",
|
||||
"core:discipline_medicine",
|
||||
"core:discipline_agriculture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_geek",
|
||||
"Name": {
|
||||
"Key": "role.geek.name",
|
||||
"Fallback": "Geek"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.geek.desc",
|
||||
"Fallback": "Consumes compute power for engineering output."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Engineering", "Add": 0, "Multiplier": 1.15 }
|
||||
],
|
||||
"RuleIds": [ "rule:geek_cooling_boost" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:geek_progress_inherit_30" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:geek_remote_work", "rule:geek_network_immunity" ]
|
||||
}
|
||||
}
|
||||
],
|
||||
"AllowedDisciplineIds": [
|
||||
"core:discipline_computer",
|
||||
"core:discipline_physics",
|
||||
"core:discipline_math",
|
||||
"core:discipline_mechanical"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_surveyor",
|
||||
"Name": {
|
||||
"Key": "role.surveyor.name",
|
||||
"Fallback": "Surveyor"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.surveyor.desc",
|
||||
"Fallback": "Collects field data and brings it back for analysis."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Activation", "Add": 0, "Multiplier": 1.10 }
|
||||
],
|
||||
"RuleIds": [ "rule:surveyor_move_speed_30" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:surveyor_extra_leads" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:surveyor_reviewer_insight" ]
|
||||
}
|
||||
}
|
||||
],
|
||||
"AllowedDisciplineIds": [
|
||||
"core:discipline_economics",
|
||||
"core:discipline_management",
|
||||
"core:discipline_law"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:role_thinker",
|
||||
"Name": {
|
||||
"Key": "role.thinker.name",
|
||||
"Fallback": "Thinker"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "role.thinker.desc",
|
||||
"Fallback": "Slow but capable of sudden leaps of insight."
|
||||
},
|
||||
"Tags": [ "role" ]
|
||||
},
|
||||
"Tiers": [
|
||||
{
|
||||
"RequiredCount": 2,
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Academic", "Add": 0, "Multiplier": 0.90 }
|
||||
],
|
||||
"RuleIds": [ "rule:thinker_epiphany_10" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 4,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:thinker_low_mood_boost" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"RequiredCount": 6,
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:thinker_masterwork" ]
|
||||
}
|
||||
}
|
||||
],
|
||||
"AllowedDisciplineIds": [
|
||||
"core:discipline_philosophy",
|
||||
"core:discipline_literature",
|
||||
"core:discipline_art"
|
||||
]
|
||||
}
|
||||
]
|
||||
283
resources/definitions/traits.json
Normal file
283
resources/definitions/traits.json
Normal file
@ -0,0 +1,283 @@
|
||||
[
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_caffeine_dependence",
|
||||
"Name": {
|
||||
"Key": "trait.caffeine.name",
|
||||
"Fallback": "Caffeine Dependence"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.caffeine.desc",
|
||||
"Fallback": "Needs coffee to work at full speed."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:trait_requires_coffee" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_night_owl",
|
||||
"Name": {
|
||||
"Key": "trait.night_owl.name",
|
||||
"Fallback": "Night Owl"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.night_owl.desc",
|
||||
"Fallback": "Shines after 18:00 and drifts in the daytime."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:trait_night_owl" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_ocd",
|
||||
"Name": {
|
||||
"Key": "trait.ocd.name",
|
||||
"Fallback": "OCD"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.ocd.desc",
|
||||
"Fallback": "Must push tasks to 100% before stopping."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:trait_ocd_full_completion" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_otaku",
|
||||
"Name": {
|
||||
"Key": "trait.otaku.name",
|
||||
"Fallback": "Otaku"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.otaku.desc",
|
||||
"Fallback": "Mood never drops near figurine decorations."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:trait_otaku_figurine" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_glass_heart",
|
||||
"Name": {
|
||||
"Key": "trait.glass_heart.name",
|
||||
"Fallback": "Glass Heart"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.glass_heart.desc",
|
||||
"Fallback": "Faints when scolded or after paper rejection."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:trait_glass_heart" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_big_eater",
|
||||
"Name": {
|
||||
"Key": "trait.big_eater.name",
|
||||
"Fallback": "Big Eater"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.big_eater.desc",
|
||||
"Fallback": "Consumes double salary but has double stamina cap."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"StatusModifiers": [
|
||||
{ "Type": "Stamina", "Add": 0, "Multiplier": 2.0 }
|
||||
],
|
||||
"RuleIds": [ "rule:trait_big_eater_salary" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_social_phobia",
|
||||
"Name": {
|
||||
"Key": "trait.social_phobia.name",
|
||||
"Fallback": "Social Phobia"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.social_phobia.desc",
|
||||
"Fallback": "Works better alone and avoids crowds."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:trait_social_phobia" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_social_butterfly",
|
||||
"Name": {
|
||||
"Key": "trait.social_butterfly.name",
|
||||
"Fallback": "Social Butterfly"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.social_butterfly.desc",
|
||||
"Fallback": "Boosts nearby teammates when chatting."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Social", "Add": 5, "Multiplier": 1.05 }
|
||||
],
|
||||
"RuleIds": [ "rule:trait_social_butterfly" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_lucky",
|
||||
"Name": {
|
||||
"Key": "trait.lucky.name",
|
||||
"Fallback": "Lucky"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.lucky.desc",
|
||||
"Fallback": "Bad events never trigger when participating."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:trait_lucky" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_unlucky",
|
||||
"Name": {
|
||||
"Key": "trait.unlucky.name",
|
||||
"Fallback": "Unlucky"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.unlucky.desc",
|
||||
"Fallback": "Bad events are more frequent but growth is faster."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Activation", "Add": 5, "Multiplier": 1.05 }
|
||||
],
|
||||
"RuleIds": [ "rule:trait_unlucky" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_keyboard_warrior",
|
||||
"Name": {
|
||||
"Key": "trait.keyboard_warrior.name",
|
||||
"Fallback": "Keyboard Warrior"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.keyboard_warrior.desc",
|
||||
"Fallback": "Excels in online PR or debate tasks."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Writing", "Add": 5, "Multiplier": 1.05 }
|
||||
],
|
||||
"RuleIds": [ "rule:trait_keyboard_warrior" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_looks_matter",
|
||||
"Name": {
|
||||
"Key": "trait.looks_matter.name",
|
||||
"Fallback": "Looks Matter"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.looks_matter.desc",
|
||||
"Fallback": "Loyalty barely drops with attractive teammates."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"StatusModifiers": [
|
||||
{ "Type": "Loyalty", "Add": 5, "Multiplier": 1.0 }
|
||||
],
|
||||
"RuleIds": [ "rule:trait_looks_matter" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_nice_guy",
|
||||
"Name": {
|
||||
"Key": "trait.nice_guy.name",
|
||||
"Fallback": "Nice Guy"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.nice_guy.desc",
|
||||
"Fallback": "Auto-helps others but overworks easily."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"StatusModifiers": [
|
||||
{ "Type": "Mood", "Add": 5, "Multiplier": 1.0 }
|
||||
],
|
||||
"RuleIds": [ "rule:trait_nice_guy" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_rebel",
|
||||
"Name": {
|
||||
"Key": "trait.rebel.name",
|
||||
"Fallback": "Rebel"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.rebel.desc",
|
||||
"Fallback": "Cannot be commanded directly but has high stats."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"AttributeModifiers": [
|
||||
{ "Type": "Academic", "Add": 10, "Multiplier": 1.0 },
|
||||
{ "Type": "Engineering", "Add": 10, "Multiplier": 1.0 },
|
||||
{ "Type": "Writing", "Add": 10, "Multiplier": 1.0 },
|
||||
{ "Type": "Financial", "Add": 10, "Multiplier": 1.0 },
|
||||
{ "Type": "Social", "Add": 10, "Multiplier": 1.0 },
|
||||
{ "Type": "Activation", "Add": 10, "Multiplier": 1.0 }
|
||||
],
|
||||
"RuleIds": [ "rule:trait_rebel" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Header": {
|
||||
"Id": "core:trait_not_human",
|
||||
"Name": {
|
||||
"Key": "trait.not_human.name",
|
||||
"Fallback": "Not Human"
|
||||
},
|
||||
"Description": {
|
||||
"Key": "trait.not_human.desc",
|
||||
"Fallback": "Needs no food or sleep but burns compute costs."
|
||||
},
|
||||
"Tags": [ "trait" ]
|
||||
},
|
||||
"Modifiers": {
|
||||
"RuleIds": [ "rule:trait_not_human" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -1,6 +1,8 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Core;
|
||||
using Models;
|
||||
|
||||
public partial class CampusController : Node2D
|
||||
{
|
||||
@ -10,13 +12,26 @@ public partial class CampusController : Node2D
|
||||
private Button _logToggle;
|
||||
|
||||
[Export] public PackedScene StudentScene { get; set; }
|
||||
[Export] public int StudentCount { get; set; } = 6;
|
||||
[Export] public int StudentCount { get; set; } = 5;
|
||||
[Export] public float CoverageStep { get; set; } = 48.0f;
|
||||
[Export] public int MaxCoveragePoints { get; set; } = 200;
|
||||
[Export] public string BehaviorConfigPath { get; set; } = "res://resources/definitions/campus_behavior.json";
|
||||
[Export] public int RandomSeed { get; set; } = 0;
|
||||
[Export] public int AssignedTaskChancePercent { get; set; } = 60;
|
||||
|
||||
private NavigationRegion2D _navigationRegion;
|
||||
private Node2D _studentsRoot;
|
||||
private readonly List<Vector2> _coveragePoints = new();
|
||||
private readonly List<CampusBehaviorAgent> _behaviorAgents = new();
|
||||
private readonly CampusBehaviorWorld _behaviorWorld = new();
|
||||
private readonly CampusLocationRegistry _locationRegistry = new();
|
||||
private CampusBehaviorConfig _behaviorConfig;
|
||||
private GameContentDatabase _contentDatabase;
|
||||
private List<string> _archetypeIds = new();
|
||||
private List<string> _roleIds = new();
|
||||
private List<string> _traitIds = new();
|
||||
private List<string> _disciplineIds = new();
|
||||
private Random _random;
|
||||
private bool _spawnPending = true;
|
||||
private bool _navBakePending = false;
|
||||
private bool _navBakeReady = false;
|
||||
@ -40,6 +55,8 @@ public partial class CampusController : Node2D
|
||||
_taskToggle.Toggled += OnTaskToggled;
|
||||
_logToggle.Toggled += OnLogToggled;
|
||||
|
||||
InitializeBehaviorAssets();
|
||||
|
||||
// 导航区域与学生容器初始化
|
||||
_navigationRegion = GetNodeOrNull<NavigationRegion2D>("Sprite2D/NavigationRegion2D");
|
||||
_studentsRoot = GetNodeOrNull<Node2D>("Students");
|
||||
@ -60,6 +77,84 @@ public partial class CampusController : Node2D
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
TrySpawnStudents();
|
||||
UpdateBehaviorAgents((float)delta);
|
||||
}
|
||||
|
||||
private void InitializeBehaviorAssets()
|
||||
{
|
||||
_behaviorConfig = CampusBehaviorConfig.Load(BehaviorConfigPath);
|
||||
_random = RandomSeed == 0 ? new Random() : new Random(RandomSeed);
|
||||
LoadContentDatabase();
|
||||
CacheLocations();
|
||||
}
|
||||
|
||||
private void LoadContentDatabase()
|
||||
{
|
||||
var registry = new ContentRegistry();
|
||||
var jsonSource = new JsonContentSource(0);
|
||||
jsonSource.DataPaths.Add("res://resources/definitions/disciplines.json");
|
||||
jsonSource.DataPaths.Add("res://resources/definitions/archetypes.json");
|
||||
jsonSource.DataPaths.Add("res://resources/definitions/roles.json");
|
||||
jsonSource.DataPaths.Add("res://resources/definitions/traits.json");
|
||||
registry.RegisterSource(jsonSource);
|
||||
|
||||
_contentDatabase = registry.BuildDatabase();
|
||||
_archetypeIds = new List<string>(_contentDatabase.Archetypes.Keys);
|
||||
_roleIds = new List<string>(_contentDatabase.Roles.Keys);
|
||||
_traitIds = new List<string>(_contentDatabase.Traits.Keys);
|
||||
_disciplineIds = new List<string>(_contentDatabase.Disciplines.Keys);
|
||||
|
||||
if (_archetypeIds.Count == 0 || _roleIds.Count == 0 || _traitIds.Count == 0)
|
||||
{
|
||||
GD.PushWarning("Behavior content definitions are missing; random tags will be limited.");
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheLocations()
|
||||
{
|
||||
var locationsRoot = GetNodeOrNull<Node2D>("Locations");
|
||||
if (locationsRoot == null)
|
||||
{
|
||||
GD.PushWarning("Campus scene is missing Locations root; agents will wander only.");
|
||||
return;
|
||||
}
|
||||
|
||||
RegisterLocation(locationsRoot, "Location_Lab", CampusLocationId.Laboratory);
|
||||
RegisterLocation(locationsRoot, "Location_Library", CampusLocationId.Library);
|
||||
RegisterLocation(locationsRoot, "Location_Canteen", CampusLocationId.Canteen);
|
||||
RegisterLocation(locationsRoot, "Location_Dorm", CampusLocationId.Dormitory);
|
||||
RegisterLocation(locationsRoot, "Location_Lake", CampusLocationId.ArtificialLake);
|
||||
RegisterLocation(locationsRoot, "Location_Coffee", CampusLocationId.CoffeeShop);
|
||||
RegisterLocation(locationsRoot, "Location_Admin", CampusLocationId.AdministrationBuilding);
|
||||
RegisterLocation(locationsRoot, "Location_Field", CampusLocationId.FootballField);
|
||||
}
|
||||
|
||||
private void RegisterLocation(Node2D root, string nodeName, CampusLocationId id)
|
||||
{
|
||||
var node = root.GetNodeOrNull<Node2D>(nodeName);
|
||||
if (node == null)
|
||||
{
|
||||
GD.PushWarning($"Campus location marker not found: {nodeName}");
|
||||
return;
|
||||
}
|
||||
|
||||
_locationRegistry.Register(id, node.GlobalPosition);
|
||||
}
|
||||
|
||||
private void UpdateBehaviorAgents(float delta)
|
||||
{
|
||||
if (_behaviorAgents.Count == 0) return;
|
||||
|
||||
_behaviorWorld.Clear();
|
||||
foreach (var agent in _behaviorAgents)
|
||||
{
|
||||
_behaviorWorld.AddOccupant(agent.Runtime.CurrentLocationId);
|
||||
}
|
||||
|
||||
foreach (var agent in _behaviorAgents)
|
||||
{
|
||||
agent.Tick(delta);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTaskToggled(bool pressed)
|
||||
@ -176,16 +271,138 @@ public partial class CampusController : Node2D
|
||||
}
|
||||
|
||||
_studentsRoot.AddChild(student);
|
||||
student.Name = $"CampusStudent_{i + 1}";
|
||||
student.SetNavigationMap(map);
|
||||
|
||||
// 随机放置在可行走区域,并设置不同的巡游起点
|
||||
var randomIndex = GD.RandRange(0, _coveragePoints.Count - 1);
|
||||
// 随机放置在可行走区域,并交给行为系统控制
|
||||
var randomIndex = _random != null
|
||||
? _random.Next(0, _coveragePoints.Count)
|
||||
: (int)GD.RandRange(0, _coveragePoints.Count - 1);
|
||||
student.GlobalPosition = _coveragePoints[randomIndex];
|
||||
student.ConfigurePatrol(_coveragePoints, i * 7);
|
||||
student.ApplyRandomTheme();
|
||||
|
||||
var runtime = BuildRandomAgentRuntime(i);
|
||||
student.Name = runtime.Name;
|
||||
|
||||
var agent = new CampusBehaviorAgent(
|
||||
student,
|
||||
runtime,
|
||||
_behaviorConfig,
|
||||
_locationRegistry,
|
||||
_behaviorWorld,
|
||||
_random,
|
||||
_coveragePoints);
|
||||
|
||||
_behaviorAgents.Add(agent);
|
||||
LogSpawn(runtime);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly CampusTaskType[] TaskTypePool =
|
||||
{
|
||||
CampusTaskType.Experiment,
|
||||
CampusTaskType.Writing,
|
||||
CampusTaskType.Administration,
|
||||
CampusTaskType.Exercise,
|
||||
CampusTaskType.Coding,
|
||||
CampusTaskType.Social
|
||||
};
|
||||
|
||||
private CampusAgentRuntime BuildRandomAgentRuntime(int index)
|
||||
{
|
||||
var rng = _random ?? Random.Shared;
|
||||
var name = $"Agent_{index + 1}";
|
||||
var student = new StudentModel(name, rng);
|
||||
AssignRandomTags(student.Core, rng);
|
||||
ApplyRandomStatus(student, rng);
|
||||
|
||||
var needs = new CampusAgentNeeds(
|
||||
rng.Next(20, 100),
|
||||
rng.Next(20, 100),
|
||||
rng.Next(20, 100),
|
||||
rng.Next(60, 100));
|
||||
|
||||
var runtime = new CampusAgentRuntime(student, needs);
|
||||
|
||||
if (rng.Next(0, 100) < AssignedTaskChancePercent && TaskTypePool.Length > 0)
|
||||
{
|
||||
var taskType = TaskTypePool[rng.Next(0, TaskTypePool.Length)];
|
||||
var duration = rng.Next(8, 16);
|
||||
runtime.AssignedTask = new CampusTask(taskType, duration);
|
||||
}
|
||||
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private void AssignRandomTags(UnitModel unit, Random rng)
|
||||
{
|
||||
unit.Tags.ArchetypeIds.Clear();
|
||||
unit.Tags.RoleIds.Clear();
|
||||
unit.Tags.TraitIds.Clear();
|
||||
|
||||
var archetypeId = PickRandomId(_archetypeIds, rng);
|
||||
if (!string.IsNullOrWhiteSpace(archetypeId))
|
||||
{
|
||||
unit.Tags.ArchetypeIds.Add(archetypeId);
|
||||
}
|
||||
|
||||
var roleId = PickRandomId(_roleIds, rng);
|
||||
if (!string.IsNullOrWhiteSpace(roleId))
|
||||
{
|
||||
unit.Tags.RoleIds.Add(roleId);
|
||||
}
|
||||
|
||||
var traitCount = _traitIds.Count == 0 ? 0 : rng.Next(1, Math.Min(3, _traitIds.Count + 1));
|
||||
for (var i = 0; i < traitCount; i++)
|
||||
{
|
||||
var traitId = PickRandomId(_traitIds, rng);
|
||||
if (!string.IsNullOrWhiteSpace(traitId) && !unit.Tags.TraitIds.Contains(traitId))
|
||||
{
|
||||
unit.Tags.TraitIds.Add(traitId);
|
||||
}
|
||||
}
|
||||
|
||||
var disciplineId = PickRandomId(_disciplineIds, rng);
|
||||
if (!string.IsNullOrWhiteSpace(disciplineId))
|
||||
{
|
||||
unit.Tags.DisciplineId = disciplineId;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRandomStatus(StudentModel student, Random rng)
|
||||
{
|
||||
var unit = student.Core;
|
||||
unit.Statuses.Stress.Current.Value = rng.Next(0, 60);
|
||||
unit.Statuses.Sanity.Current.Value = rng.Next(50, 100);
|
||||
unit.Statuses.Mood.Value = rng.Next(30, 90);
|
||||
student.Progress.Stamina.Current.Value = rng.Next(25, 100);
|
||||
}
|
||||
|
||||
private static string PickRandomId(List<string> ids, Random rng)
|
||||
{
|
||||
if (ids == null || ids.Count == 0) return null;
|
||||
return ids[rng.Next(0, ids.Count)];
|
||||
}
|
||||
|
||||
private void LogSpawn(CampusAgentRuntime runtime)
|
||||
{
|
||||
var unit = runtime.Unit;
|
||||
var archetypes = unit.Tags.ArchetypeIds.Count == 0 ? "none" : string.Join(",", unit.Tags.ArchetypeIds);
|
||||
var roles = unit.Tags.RoleIds.Count == 0 ? "none" : string.Join(",", unit.Tags.RoleIds);
|
||||
var traits = unit.Tags.TraitIds.Count == 0 ? "none" : string.Join(",", unit.Tags.TraitIds);
|
||||
var taskInfo = runtime.AssignedTask != null
|
||||
? $"{runtime.AssignedTask.Type} ({runtime.AssignedTask.RemainingSeconds:0}s)"
|
||||
: "none";
|
||||
var attributes =
|
||||
$"A:{unit.Attributes.Academic.DisplayInt()} " +
|
||||
$"E:{unit.Attributes.Engineering.DisplayInt()} " +
|
||||
$"W:{unit.Attributes.Writing.DisplayInt()} " +
|
||||
$"F:{unit.Attributes.Financial.DisplayInt()} " +
|
||||
$"S:{unit.Attributes.Social.DisplayInt()} " +
|
||||
$"Act:{unit.Attributes.Activation.DisplayInt()}";
|
||||
|
||||
GD.Print($"[CampusAI] Spawned {runtime.Name} archetype={archetypes} role={roles} traits={traits} task={taskInfo} attrs={attributes}");
|
||||
}
|
||||
|
||||
private List<Vector2> BuildCoveragePoints()
|
||||
{
|
||||
var points = new List<Vector2>();
|
||||
|
||||
@ -2019,7 +2019,6 @@ offset_right = 955.0
|
||||
offset_bottom = 535.0
|
||||
|
||||
[node name="TopBar" parent="." instance=ExtResource("2_p4tmp")]
|
||||
visible = false
|
||||
offset_bottom = 55.0
|
||||
|
||||
[node name="Task" parent="." instance=ExtResource("3_4gjr3")]
|
||||
@ -2032,3 +2031,29 @@ offset_bottom = 455.0
|
||||
[node name="TileMapLayer" type="TileMapLayer" parent="."]
|
||||
visible = false
|
||||
tile_set = SubResource("TileSet_74kl0")
|
||||
|
||||
[node name="Locations" type="Node2D" parent="."]
|
||||
|
||||
[node name="Location_Lab" type="Node2D" parent="Locations"]
|
||||
position = Vector2(150, 196)
|
||||
|
||||
[node name="Location_Library" type="Node2D" parent="Locations"]
|
||||
position = Vector2(440, 196)
|
||||
|
||||
[node name="Location_Canteen" type="Node2D" parent="Locations"]
|
||||
position = Vector2(745, 166)
|
||||
|
||||
[node name="Location_Dorm" type="Node2D" parent="Locations"]
|
||||
position = Vector2(848, 192)
|
||||
|
||||
[node name="Location_Lake" type="Node2D" parent="Locations"]
|
||||
position = Vector2(943, 179)
|
||||
|
||||
[node name="Location_Coffee" type="Node2D" parent="Locations"]
|
||||
position = Vector2(160, 395)
|
||||
|
||||
[node name="Location_Admin" type="Node2D" parent="Locations"]
|
||||
position = Vector2(296, 452)
|
||||
|
||||
[node name="Location_Field" type="Node2D" parent="Locations"]
|
||||
position = Vector2(560, 300)
|
||||
|
||||
778
scripts/Campus/CampusBehaviorAgent.cs
Normal file
778
scripts/Campus/CampusBehaviorAgent.cs
Normal file
@ -0,0 +1,778 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
using Models;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime data for a campus agent. This keeps Godot nodes and pure data separate
|
||||
/// so the behavior system can be tested without scene dependencies.
|
||||
/// </summary>
|
||||
public sealed class CampusAgentRuntime
|
||||
{
|
||||
public StudentModel Student { get; }
|
||||
public CampusAgentNeeds Needs { get; }
|
||||
public CampusTask AssignedTask { get; set; }
|
||||
public CampusLocationId CurrentLocationId { get; set; } = CampusLocationId.None;
|
||||
|
||||
public UnitModel Unit => Student.Core;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => Student.Name;
|
||||
set => Student.Name = value;
|
||||
}
|
||||
|
||||
public CampusAgentRuntime(StudentModel student, CampusAgentNeeds needs)
|
||||
{
|
||||
Student = student ?? throw new ArgumentNullException(nameof(student));
|
||||
Needs = needs ?? throw new ArgumentNullException(nameof(needs));
|
||||
}
|
||||
|
||||
public bool HasTrait(string traitId) => Unit.Tags.TraitIds.Contains(traitId);
|
||||
public bool HasRole(string roleId) => Unit.Tags.RoleIds.Contains(roleId);
|
||||
public bool HasArchetype(string archetypeId) => Unit.Tags.ArchetypeIds.Contains(archetypeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intent produced by the planner. It captures both the action and the destination
|
||||
/// so the state machine can move and execute without re-running the decision logic.
|
||||
/// </summary>
|
||||
public sealed class CampusBehaviorIntent
|
||||
{
|
||||
public CampusBehaviorPriority Priority { get; }
|
||||
public CampusActionId ActionId { get; }
|
||||
public CampusLocationId LocationId { get; }
|
||||
public string Reason { get; }
|
||||
|
||||
public CampusBehaviorIntent(CampusBehaviorPriority priority, CampusActionId actionId, CampusLocationId locationId, string reason)
|
||||
{
|
||||
Priority = priority;
|
||||
ActionId = actionId;
|
||||
LocationId = locationId;
|
||||
Reason = reason ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool Matches(CampusBehaviorIntent other)
|
||||
{
|
||||
if (other == null) return false;
|
||||
return Priority == other.Priority && ActionId == other.ActionId && LocationId == other.LocationId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared context passed into providers/states so they can evaluate the same data
|
||||
/// without hard-coding dependencies.
|
||||
/// </summary>
|
||||
public sealed class CampusBehaviorContext
|
||||
{
|
||||
public CampusAgentRuntime Agent { get; }
|
||||
public CampusBehaviorConfig Config { get; }
|
||||
public CampusLocationRegistry Locations { get; }
|
||||
public CampusBehaviorWorld World { get; }
|
||||
public Random Rng { get; }
|
||||
public List<Vector2> WanderPoints { get; }
|
||||
|
||||
public CampusBehaviorContext(
|
||||
CampusAgentRuntime agent,
|
||||
CampusBehaviorConfig config,
|
||||
CampusLocationRegistry locations,
|
||||
CampusBehaviorWorld world,
|
||||
Random rng,
|
||||
List<Vector2> wanderPoints)
|
||||
{
|
||||
Agent = agent;
|
||||
Config = config;
|
||||
Locations = locations;
|
||||
World = world;
|
||||
Rng = rng;
|
||||
WanderPoints = wanderPoints;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Providers represent a single rule in the priority queue. Each provider returns
|
||||
/// a behavior intent or null if it cannot apply to the current context.
|
||||
/// </summary>
|
||||
public interface ICampusBehaviorProvider
|
||||
{
|
||||
CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Critical state provider: handles sanity collapse, extreme stress, or exhaustion.
|
||||
/// This is the highest priority in the decision queue.
|
||||
/// </summary>
|
||||
public sealed class CriticalBehaviorProvider : ICampusBehaviorProvider
|
||||
{
|
||||
private readonly string _grinderArchetypeId;
|
||||
|
||||
public CriticalBehaviorProvider(string grinderArchetypeId)
|
||||
{
|
||||
_grinderArchetypeId = grinderArchetypeId;
|
||||
}
|
||||
|
||||
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context)
|
||||
{
|
||||
var agent = context.Agent;
|
||||
var config = context.Config;
|
||||
var stamina = agent.Student.Progress.Stamina.Current.Value;
|
||||
var sanity = agent.Unit.Statuses.Sanity.Current.Value;
|
||||
var stress = agent.Unit.Statuses.Stress.Current.Value;
|
||||
|
||||
if (sanity <= config.CriticalSanityThreshold || stress >= config.CriticalStressThreshold)
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Critical,
|
||||
CampusActionId.Staring,
|
||||
CampusLocationId.ArtificialLake,
|
||||
"critical_sanity_or_stress");
|
||||
}
|
||||
|
||||
if (stamina <= config.CriticalStaminaThreshold)
|
||||
{
|
||||
if (agent.HasArchetype(_grinderArchetypeId) && context.Rng.NextDouble() < 0.5)
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Critical,
|
||||
CampusActionId.CoffeeBreak,
|
||||
CampusLocationId.CoffeeShop,
|
||||
"grinder_forced_coffee");
|
||||
}
|
||||
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Critical,
|
||||
CampusActionId.Sleeping,
|
||||
CampusLocationId.Dormitory,
|
||||
"critical_stamina");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigned task provider: if the agent has a task, it is executed before needs.
|
||||
/// </summary>
|
||||
public sealed class AssignedTaskBehaviorProvider : ICampusBehaviorProvider
|
||||
{
|
||||
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context)
|
||||
{
|
||||
var task = context.Agent.AssignedTask;
|
||||
if (task == null || task.IsComplete) return null;
|
||||
|
||||
var action = MapTaskToAction(task.Type);
|
||||
var location = context.Config.GetActionConfig(action)?.LocationId ?? CampusLocationId.Laboratory;
|
||||
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.AssignedTask,
|
||||
action,
|
||||
location,
|
||||
$"assigned_task_{task.Type}");
|
||||
}
|
||||
|
||||
public static CampusActionId MapTaskToAction(CampusTaskType taskType)
|
||||
{
|
||||
return taskType switch
|
||||
{
|
||||
CampusTaskType.Experiment => CampusActionId.Experimenting,
|
||||
CampusTaskType.Writing => CampusActionId.Writing,
|
||||
CampusTaskType.Administration => CampusActionId.Administration,
|
||||
CampusTaskType.Exercise => CampusActionId.Running,
|
||||
CampusTaskType.Coding => CampusActionId.Experimenting,
|
||||
CampusTaskType.Social => CampusActionId.Socializing,
|
||||
_ => CampusActionId.Wandering
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needs provider: hunger, fatigue, mood, and social needs are handled here.
|
||||
/// It sits below assigned tasks but above trait-driven idle behavior.
|
||||
/// </summary>
|
||||
public sealed class NeedsBehaviorProvider : ICampusBehaviorProvider
|
||||
{
|
||||
private readonly string _traitNotHumanId;
|
||||
private readonly string _traitCaffeineId;
|
||||
private readonly string _grinderArchetypeId;
|
||||
|
||||
public NeedsBehaviorProvider(string traitNotHumanId, string traitCaffeineId, string grinderArchetypeId)
|
||||
{
|
||||
_traitNotHumanId = traitNotHumanId;
|
||||
_traitCaffeineId = traitCaffeineId;
|
||||
_grinderArchetypeId = grinderArchetypeId;
|
||||
}
|
||||
|
||||
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context)
|
||||
{
|
||||
var agent = context.Agent;
|
||||
var config = context.Config;
|
||||
var needs = agent.Needs;
|
||||
|
||||
var isNotHuman = agent.HasTrait(_traitNotHumanId);
|
||||
|
||||
if (!isNotHuman && needs.Hunger.Value <= config.HungerThreshold)
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Needs,
|
||||
CampusActionId.Eating,
|
||||
CampusLocationId.Canteen,
|
||||
"need_hunger");
|
||||
}
|
||||
|
||||
var stamina = agent.Student.Progress.Stamina.Current.Value;
|
||||
if (!isNotHuman && (needs.Energy.Value <= config.EnergyThreshold || stamina <= config.CriticalStaminaThreshold))
|
||||
{
|
||||
if (agent.HasTrait(_traitCaffeineId))
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Needs,
|
||||
CampusActionId.CoffeeBreak,
|
||||
CampusLocationId.CoffeeShop,
|
||||
"need_caffeine");
|
||||
}
|
||||
|
||||
if (agent.HasArchetype(_grinderArchetypeId) && context.Rng.NextDouble() < 0.5)
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Needs,
|
||||
CampusActionId.CoffeeBreak,
|
||||
CampusLocationId.CoffeeShop,
|
||||
"grinder_refuse_sleep");
|
||||
}
|
||||
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Needs,
|
||||
CampusActionId.Sleeping,
|
||||
CampusLocationId.Dormitory,
|
||||
"need_sleep");
|
||||
}
|
||||
|
||||
var mood = agent.Unit.Statuses.Mood.Value;
|
||||
if (mood <= config.LowMoodThreshold)
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Needs,
|
||||
CampusActionId.Chilling,
|
||||
CampusLocationId.Dormitory,
|
||||
"need_mood");
|
||||
}
|
||||
|
||||
if (needs.Social.Value <= config.SocialThreshold)
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Needs,
|
||||
CampusActionId.Socializing,
|
||||
CampusLocationId.CoffeeShop,
|
||||
"need_social");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trait-driven provider: applies long-term personality or tag tendencies when
|
||||
/// there is no urgent need.
|
||||
/// </summary>
|
||||
public sealed class TraitBehaviorProvider : ICampusBehaviorProvider
|
||||
{
|
||||
private readonly string _archetypeGrinderId;
|
||||
private readonly string _archetypeSlackerId;
|
||||
private readonly string _traitSocialPhobiaId;
|
||||
private readonly string _traitSocialButterflyId;
|
||||
private readonly string _roleLabRatId;
|
||||
private readonly string _roleAlchemistId;
|
||||
private readonly string _roleWriterId;
|
||||
private readonly string _roleScribeId;
|
||||
|
||||
public TraitBehaviorProvider(
|
||||
string archetypeGrinderId,
|
||||
string archetypeSlackerId,
|
||||
string traitSocialPhobiaId,
|
||||
string traitSocialButterflyId,
|
||||
string roleLabRatId,
|
||||
string roleAlchemistId,
|
||||
string roleWriterId,
|
||||
string roleScribeId)
|
||||
{
|
||||
_archetypeGrinderId = archetypeGrinderId;
|
||||
_archetypeSlackerId = archetypeSlackerId;
|
||||
_traitSocialPhobiaId = traitSocialPhobiaId;
|
||||
_traitSocialButterflyId = traitSocialButterflyId;
|
||||
_roleLabRatId = roleLabRatId;
|
||||
_roleAlchemistId = roleAlchemistId;
|
||||
_roleWriterId = roleWriterId;
|
||||
_roleScribeId = roleScribeId;
|
||||
}
|
||||
|
||||
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context)
|
||||
{
|
||||
var agent = context.Agent;
|
||||
|
||||
if (agent.HasTrait(_traitSocialPhobiaId))
|
||||
{
|
||||
var quietSpot = PickLeastCrowded(context, CampusLocationId.ArtificialLake, CampusLocationId.Dormitory, CampusLocationId.Library);
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Trait,
|
||||
CampusActionId.Staring,
|
||||
quietSpot,
|
||||
"trait_social_phobia");
|
||||
}
|
||||
|
||||
if (agent.HasTrait(_traitSocialButterflyId))
|
||||
{
|
||||
var socialSpot = PickMostCrowded(context, CampusLocationId.Canteen, CampusLocationId.CoffeeShop);
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Trait,
|
||||
CampusActionId.Socializing,
|
||||
socialSpot,
|
||||
"trait_social_butterfly");
|
||||
}
|
||||
|
||||
if (agent.HasArchetype(_archetypeGrinderId) || agent.HasRole(_roleLabRatId) || agent.HasRole(_roleAlchemistId))
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Trait,
|
||||
CampusActionId.Experimenting,
|
||||
CampusLocationId.Laboratory,
|
||||
"trait_prefers_lab");
|
||||
}
|
||||
|
||||
if (agent.HasRole(_roleWriterId) || agent.HasRole(_roleScribeId))
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Trait,
|
||||
CampusActionId.Writing,
|
||||
CampusLocationId.Library,
|
||||
"trait_prefers_library");
|
||||
}
|
||||
|
||||
if (agent.HasArchetype(_archetypeSlackerId))
|
||||
{
|
||||
var action = context.Rng.NextDouble() < 0.3
|
||||
? CampusActionId.Eating
|
||||
: CampusActionId.Chilling;
|
||||
var location = action == CampusActionId.Eating ? CampusLocationId.Canteen : CampusLocationId.Dormitory;
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Trait,
|
||||
action,
|
||||
location,
|
||||
"trait_slacker");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CampusLocationId PickLeastCrowded(CampusBehaviorContext context, params CampusLocationId[] candidates)
|
||||
{
|
||||
if (candidates == null || candidates.Length == 0) return CampusLocationId.ArtificialLake;
|
||||
|
||||
var best = candidates[0];
|
||||
var bestCount = context.World.GetOccupancy(best);
|
||||
for (var i = 1; i < candidates.Length; i++)
|
||||
{
|
||||
var count = context.World.GetOccupancy(candidates[i]);
|
||||
if (count < bestCount)
|
||||
{
|
||||
best = candidates[i];
|
||||
bestCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static CampusLocationId PickMostCrowded(CampusBehaviorContext context, params CampusLocationId[] candidates)
|
||||
{
|
||||
if (candidates == null || candidates.Length == 0) return CampusLocationId.Canteen;
|
||||
|
||||
var best = candidates[0];
|
||||
var bestCount = context.World.GetOccupancy(best);
|
||||
for (var i = 1; i < candidates.Length; i++)
|
||||
{
|
||||
var count = context.World.GetOccupancy(candidates[i]);
|
||||
if (count > bestCount)
|
||||
{
|
||||
best = candidates[i];
|
||||
bestCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Idle provider: default fallback when nothing else applies.
|
||||
/// </summary>
|
||||
public sealed class IdleBehaviorProvider : ICampusBehaviorProvider
|
||||
{
|
||||
public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context)
|
||||
{
|
||||
return new CampusBehaviorIntent(
|
||||
CampusBehaviorPriority.Idle,
|
||||
CampusActionId.Wandering,
|
||||
CampusLocationId.RandomWander,
|
||||
"idle_wander");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Planner executes providers in priority order. This lets us add or remove
|
||||
/// providers without editing the state machine.
|
||||
/// </summary>
|
||||
public sealed class CampusBehaviorPlanner
|
||||
{
|
||||
private readonly List<ICampusBehaviorProvider> _providers;
|
||||
|
||||
public CampusBehaviorPlanner(IEnumerable<ICampusBehaviorProvider> providers)
|
||||
{
|
||||
_providers = new List<ICampusBehaviorProvider>(providers ?? Array.Empty<ICampusBehaviorProvider>());
|
||||
}
|
||||
|
||||
public CampusBehaviorIntent PickIntent(CampusBehaviorContext context)
|
||||
{
|
||||
foreach (var provider in _providers)
|
||||
{
|
||||
var intent = provider.TryCreateIntent(context);
|
||||
if (intent != null)
|
||||
{
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
||||
return new CampusBehaviorIntent(CampusBehaviorPriority.Idle, CampusActionId.Wandering, CampusLocationId.RandomWander, "fallback_idle");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State interface for the AI FSM. Each state can transition by requesting
|
||||
/// a change via the owning behavior agent.
|
||||
/// </summary>
|
||||
public interface ICampusBehaviorState
|
||||
{
|
||||
void Enter(CampusBehaviorAgent agent);
|
||||
void Tick(CampusBehaviorAgent agent, float delta);
|
||||
void Exit(CampusBehaviorAgent agent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State machine wrapper to enforce enter/exit semantics.
|
||||
/// </summary>
|
||||
public sealed class CampusBehaviorStateMachine
|
||||
{
|
||||
private ICampusBehaviorState _current;
|
||||
|
||||
public void ChangeState(CampusBehaviorAgent agent, ICampusBehaviorState next)
|
||||
{
|
||||
_current?.Exit(agent);
|
||||
_current = next;
|
||||
_current?.Enter(agent);
|
||||
}
|
||||
|
||||
public void Tick(CampusBehaviorAgent agent, float delta)
|
||||
{
|
||||
_current?.Tick(agent, delta);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decision state: pick a new intent and immediately transition to movement.
|
||||
/// This keeps the intent selection isolated and easy to extend.
|
||||
/// </summary>
|
||||
public sealed class CampusDecisionState : ICampusBehaviorState
|
||||
{
|
||||
public void Enter(CampusBehaviorAgent agent)
|
||||
{
|
||||
var intent = agent.PlanNextIntent();
|
||||
agent.StartIntent(intent);
|
||||
}
|
||||
|
||||
public void Tick(CampusBehaviorAgent agent, float delta)
|
||||
{
|
||||
}
|
||||
|
||||
public void Exit(CampusBehaviorAgent agent)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Movement state: navigate to the intent's target location.
|
||||
/// Once the agent arrives, it transitions into the action state.
|
||||
/// </summary>
|
||||
public sealed class CampusMoveState : ICampusBehaviorState
|
||||
{
|
||||
private CampusBehaviorIntent _intent;
|
||||
|
||||
public CampusMoveState(CampusBehaviorIntent intent)
|
||||
{
|
||||
_intent = intent;
|
||||
}
|
||||
|
||||
public void Enter(CampusBehaviorAgent agent)
|
||||
{
|
||||
var target = agent.ResolveTargetPosition(_intent);
|
||||
agent.Student.SetBehaviorTarget(target);
|
||||
agent.Log($"moving_to {target} action={_intent.ActionId} reason={_intent.Reason}");
|
||||
}
|
||||
|
||||
public void Tick(CampusBehaviorAgent agent, float delta)
|
||||
{
|
||||
if (agent.Student.HasReachedBehaviorTarget())
|
||||
{
|
||||
agent.Student.ClearBehaviorTarget();
|
||||
agent.StateMachine.ChangeState(agent, new CampusActionState(_intent));
|
||||
}
|
||||
}
|
||||
|
||||
public void Exit(CampusBehaviorAgent agent)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action state: apply per-second deltas and update task progress.
|
||||
/// When the action duration expires, transition back to decision.
|
||||
/// </summary>
|
||||
public sealed class CampusActionState : ICampusBehaviorState
|
||||
{
|
||||
private readonly CampusBehaviorIntent _intent;
|
||||
private float _remaining;
|
||||
private CampusActionConfig _actionConfig;
|
||||
private float _logTimer;
|
||||
|
||||
public CampusActionState(CampusBehaviorIntent intent)
|
||||
{
|
||||
_intent = intent;
|
||||
}
|
||||
|
||||
public void Enter(CampusBehaviorAgent agent)
|
||||
{
|
||||
_actionConfig = agent.Config.GetActionConfig(_intent.ActionId);
|
||||
_remaining = _actionConfig?.DurationSeconds ?? 4.0f;
|
||||
_logTimer = 0.0f;
|
||||
|
||||
agent.Runtime.CurrentLocationId = _intent.LocationId;
|
||||
agent.Log($"action_start {_intent.ActionId} duration={_remaining:0.0}s reason={_intent.Reason}");
|
||||
}
|
||||
|
||||
public void Tick(CampusBehaviorAgent agent, float delta)
|
||||
{
|
||||
if (_remaining <= 0.0f)
|
||||
{
|
||||
agent.StateMachine.ChangeState(agent, new CampusDecisionState());
|
||||
return;
|
||||
}
|
||||
|
||||
if (_actionConfig != null)
|
||||
{
|
||||
ApplyActionDelta(agent, delta, _actionConfig);
|
||||
}
|
||||
|
||||
AdvanceTask(agent, delta);
|
||||
|
||||
_remaining -= delta;
|
||||
_logTimer += delta;
|
||||
if (_logTimer >= 1.5f)
|
||||
{
|
||||
_logTimer = 0.0f;
|
||||
agent.Log($"action_tick {_intent.ActionId} remaining={Mathf.Max(0.0f, _remaining):0.0}s");
|
||||
}
|
||||
}
|
||||
|
||||
public void Exit(CampusBehaviorAgent agent)
|
||||
{
|
||||
agent.Runtime.CurrentLocationId = CampusLocationId.None;
|
||||
agent.Log($"action_end {_intent.ActionId}");
|
||||
}
|
||||
|
||||
private static void ApplyActionDelta(CampusBehaviorAgent agent, float delta, CampusActionConfig action)
|
||||
{
|
||||
var needs = agent.Runtime.Needs;
|
||||
needs.Hunger.Add(action.HungerDelta * delta);
|
||||
needs.Energy.Add(action.EnergyDelta * delta);
|
||||
needs.Social.Add(action.SocialDelta * delta);
|
||||
needs.Health.Add(action.HealthDelta * delta);
|
||||
|
||||
agent.Runtime.Student.Progress.Stamina.Add(action.StaminaDelta * delta);
|
||||
agent.Runtime.Unit.Statuses.Stress.Add(action.StressDelta * delta);
|
||||
agent.Runtime.Unit.Statuses.Sanity.Add(action.SanityDelta * delta);
|
||||
agent.Runtime.Unit.Statuses.Mood.Add(action.MoodDelta * delta);
|
||||
}
|
||||
|
||||
private void AdvanceTask(CampusBehaviorAgent agent, float delta)
|
||||
{
|
||||
var task = agent.Runtime.AssignedTask;
|
||||
if (task == null) return;
|
||||
|
||||
var actionForTask = AssignedTaskBehaviorProvider.MapTaskToAction(task.Type);
|
||||
if (actionForTask != _intent.ActionId) return;
|
||||
|
||||
task.Advance(delta);
|
||||
if (task.IsComplete)
|
||||
{
|
||||
agent.Log($"task_complete {task.Type}");
|
||||
agent.Runtime.AssignedTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main behavior agent that drives one campus character. It owns the planner,
|
||||
/// state machine, and applies baseline stat decay on every tick.
|
||||
/// </summary>
|
||||
public sealed class CampusBehaviorAgent
|
||||
{
|
||||
public CampusStudent Student { get; }
|
||||
public CampusAgentRuntime Runtime { get; }
|
||||
public CampusBehaviorConfig Config { get; }
|
||||
public CampusBehaviorStateMachine StateMachine { get; } = new();
|
||||
|
||||
private readonly CampusBehaviorPlanner _planner;
|
||||
private readonly CampusBehaviorContext _context;
|
||||
private CampusBehaviorIntent _currentIntent;
|
||||
private float _decisionTimer;
|
||||
|
||||
public CampusBehaviorAgent(
|
||||
CampusStudent student,
|
||||
CampusAgentRuntime runtime,
|
||||
CampusBehaviorConfig config,
|
||||
CampusLocationRegistry locations,
|
||||
CampusBehaviorWorld world,
|
||||
Random rng,
|
||||
List<Vector2> wanderPoints)
|
||||
{
|
||||
Student = student ?? throw new ArgumentNullException(nameof(student));
|
||||
Runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
Config = config ?? new CampusBehaviorConfig();
|
||||
|
||||
_context = new CampusBehaviorContext(runtime, Config, locations, world, rng ?? new Random(), wanderPoints ?? new List<Vector2>());
|
||||
_planner = new CampusBehaviorPlanner(BuildProviders());
|
||||
|
||||
Student.EnableBehaviorControl();
|
||||
StateMachine.ChangeState(this, new CampusDecisionState());
|
||||
}
|
||||
|
||||
public void Tick(float delta)
|
||||
{
|
||||
ApplyBaselineDecay(delta);
|
||||
TryInterrupt(delta);
|
||||
StateMachine.Tick(this, delta);
|
||||
}
|
||||
|
||||
public CampusBehaviorIntent PlanNextIntent()
|
||||
{
|
||||
var intent = _planner.PickIntent(_context);
|
||||
Log($"intent {intent.Priority} {intent.ActionId} -> {intent.LocationId} reason={intent.Reason}");
|
||||
return intent;
|
||||
}
|
||||
|
||||
public void StartIntent(CampusBehaviorIntent intent)
|
||||
{
|
||||
_currentIntent = intent;
|
||||
StateMachine.ChangeState(this, new CampusMoveState(intent));
|
||||
}
|
||||
|
||||
public Vector2 ResolveTargetPosition(CampusBehaviorIntent intent)
|
||||
{
|
||||
if (intent.LocationId == CampusLocationId.RandomWander)
|
||||
{
|
||||
return PickRandomWanderPoint();
|
||||
}
|
||||
|
||||
if (_context.Locations != null && _context.Locations.TryGetPosition(intent.LocationId, out var position))
|
||||
{
|
||||
return position;
|
||||
}
|
||||
|
||||
return PickRandomWanderPoint();
|
||||
}
|
||||
|
||||
public void Log(string message)
|
||||
{
|
||||
GD.Print($"[CampusAI:{Runtime.Name}] {message}");
|
||||
}
|
||||
|
||||
private void ApplyBaselineDecay(float delta)
|
||||
{
|
||||
var needs = Runtime.Needs;
|
||||
var isNotHuman = Runtime.HasTrait(CampusTraitIds.NotHuman);
|
||||
var hungerDecay = Config.HungerDecayPerSecond;
|
||||
|
||||
if (Runtime.HasTrait(CampusTraitIds.BigEater))
|
||||
{
|
||||
hungerDecay *= 1.5f;
|
||||
}
|
||||
|
||||
if (!isNotHuman)
|
||||
{
|
||||
needs.Hunger.Add(-hungerDecay * delta);
|
||||
needs.Energy.Add(-Config.EnergyDecayPerSecond * delta);
|
||||
Runtime.Student.Progress.Stamina.Add(-Config.StaminaDecayPerSecond * delta);
|
||||
}
|
||||
|
||||
needs.Social.Add(-Config.SocialDecayPerSecond * delta);
|
||||
Runtime.Unit.Statuses.Stress.Add(Config.StressGrowthPerSecond * delta);
|
||||
}
|
||||
|
||||
private void TryInterrupt(float delta)
|
||||
{
|
||||
_decisionTimer += delta;
|
||||
if (_decisionTimer < Config.DecisionIntervalSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_decisionTimer = 0.0f;
|
||||
var candidate = _planner.PickIntent(_context);
|
||||
if (candidate == null) return;
|
||||
|
||||
if (_currentIntent == null || candidate.Priority < _currentIntent.Priority)
|
||||
{
|
||||
Log($"interrupt {candidate.Priority} {candidate.ActionId} reason={candidate.Reason}");
|
||||
StartIntent(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 PickRandomWanderPoint()
|
||||
{
|
||||
if (_context.WanderPoints == null || _context.WanderPoints.Count == 0)
|
||||
{
|
||||
return Student.GlobalPosition;
|
||||
}
|
||||
|
||||
var index = _context.Rng.Next(0, _context.WanderPoints.Count);
|
||||
return _context.WanderPoints[index];
|
||||
}
|
||||
|
||||
private IEnumerable<ICampusBehaviorProvider> BuildProviders()
|
||||
{
|
||||
yield return new CriticalBehaviorProvider(CoreIds.ArchetypeGrinder);
|
||||
yield return new AssignedTaskBehaviorProvider();
|
||||
yield return new NeedsBehaviorProvider(CampusTraitIds.NotHuman, CampusTraitIds.CaffeineDependence, CoreIds.ArchetypeGrinder);
|
||||
yield return new TraitBehaviorProvider(
|
||||
CoreIds.ArchetypeGrinder,
|
||||
CoreIds.ArchetypeSlacker,
|
||||
CampusTraitIds.SocialPhobia,
|
||||
CampusTraitIds.SocialButterfly,
|
||||
CoreIds.RoleLabRat,
|
||||
CoreIds.RoleAlchemist,
|
||||
CoreIds.RoleWriter,
|
||||
CoreIds.RoleScribe);
|
||||
yield return new IdleBehaviorProvider();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Centralized IDs for traits referenced by the behavior system.
|
||||
/// Keeping them here avoids scattering magic strings.
|
||||
/// </summary>
|
||||
public static class CampusTraitIds
|
||||
{
|
||||
public const string CaffeineDependence = "core:trait_caffeine_dependence";
|
||||
public const string SocialPhobia = "core:trait_social_phobia";
|
||||
public const string SocialButterfly = "core:trait_social_butterfly";
|
||||
public const string NotHuman = "core:trait_not_human";
|
||||
public const string BigEater = "core:trait_big_eater";
|
||||
}
|
||||
1
scripts/Campus/CampusBehaviorAgent.cs.uid
Normal file
1
scripts/Campus/CampusBehaviorAgent.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bd1bjq6vrs4hh
|
||||
269
scripts/Campus/CampusBehaviorConfig.cs
Normal file
269
scripts/Campus/CampusBehaviorConfig.cs
Normal file
@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Godot;
|
||||
using Models;
|
||||
|
||||
/// <summary>
|
||||
/// Location identifiers used by the campus behavior system.
|
||||
/// These map to Node2D markers in campus.tscn so the AI can pick targets by name.
|
||||
/// </summary>
|
||||
public enum CampusLocationId
|
||||
{
|
||||
None,
|
||||
Laboratory,
|
||||
Library,
|
||||
Canteen,
|
||||
Dormitory,
|
||||
ArtificialLake,
|
||||
CoffeeShop,
|
||||
AdministrationBuilding,
|
||||
FootballField,
|
||||
RandomWander
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action identifiers used by the behavior planner and state machine.
|
||||
/// Each action is configured via campus_behavior.json for duration and stat deltas.
|
||||
/// </summary>
|
||||
public enum CampusActionId
|
||||
{
|
||||
None,
|
||||
Experimenting,
|
||||
Writing,
|
||||
Eating,
|
||||
Sleeping,
|
||||
Chilling,
|
||||
Staring,
|
||||
CoffeeBreak,
|
||||
Administration,
|
||||
Running,
|
||||
Socializing,
|
||||
Wandering
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Priority levels match the design doc ordering: lower value = higher priority.
|
||||
/// </summary>
|
||||
public enum CampusBehaviorPriority
|
||||
{
|
||||
Critical = 0,
|
||||
AssignedTask = 1,
|
||||
Needs = 2,
|
||||
Trait = 3,
|
||||
Idle = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal task types for the campus demo. These are not full gameplay tasks,
|
||||
/// just drivers for the assigned-task priority in the AI.
|
||||
/// </summary>
|
||||
public enum CampusTaskType
|
||||
{
|
||||
Experiment,
|
||||
Writing,
|
||||
Administration,
|
||||
Exercise,
|
||||
Coding,
|
||||
Social
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action configuration loaded from JSON. Deltas are applied per second while
|
||||
/// the action is running, so longer actions accumulate more effect.
|
||||
/// </summary>
|
||||
public sealed class CampusActionConfig
|
||||
{
|
||||
public CampusActionId ActionId { get; set; }
|
||||
public CampusLocationId LocationId { get; set; }
|
||||
public float DurationSeconds { get; set; }
|
||||
public float HungerDelta { get; set; }
|
||||
public float EnergyDelta { get; set; }
|
||||
public float StaminaDelta { get; set; }
|
||||
public float StressDelta { get; set; }
|
||||
public float MoodDelta { get; set; }
|
||||
public float SocialDelta { get; set; }
|
||||
public float SanityDelta { get; set; }
|
||||
public float HealthDelta { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Global behavior configuration for campus AI. This is intentionally data-driven
|
||||
/// so balancing can happen in JSON without touching code.
|
||||
/// </summary>
|
||||
public sealed class CampusBehaviorConfig
|
||||
{
|
||||
public float CriticalSanityThreshold { get; set; } = 15f;
|
||||
public float CriticalStaminaThreshold { get; set; } = 12f;
|
||||
public float CriticalStressThreshold { get; set; } = 90f;
|
||||
public float HungerThreshold { get; set; } = 30f;
|
||||
public float EnergyThreshold { get; set; } = 25f;
|
||||
public float SocialThreshold { get; set; } = 35f;
|
||||
public float LowMoodThreshold { get; set; } = 25f;
|
||||
public float HungerDecayPerSecond { get; set; } = 0.6f;
|
||||
public float EnergyDecayPerSecond { get; set; } = 0.5f;
|
||||
public float StaminaDecayPerSecond { get; set; } = 0.4f;
|
||||
public float StressGrowthPerSecond { get; set; } = 0.45f;
|
||||
public float SocialDecayPerSecond { get; set; } = 0.35f;
|
||||
public float DecisionIntervalSeconds { get; set; } = 0.5f;
|
||||
public List<CampusActionConfig> ActionConfigs { get; set; } = new();
|
||||
|
||||
private readonly Dictionary<CampusActionId, CampusActionConfig> _actionLookup = new();
|
||||
|
||||
public CampusActionConfig GetActionConfig(CampusActionId id)
|
||||
{
|
||||
if (_actionLookup.Count == 0)
|
||||
{
|
||||
BuildLookup();
|
||||
}
|
||||
|
||||
return _actionLookup.TryGetValue(id, out var config) ? config : null;
|
||||
}
|
||||
|
||||
private void BuildLookup()
|
||||
{
|
||||
_actionLookup.Clear();
|
||||
if (ActionConfigs == null) return;
|
||||
foreach (var config in ActionConfigs)
|
||||
{
|
||||
_actionLookup[config.ActionId] = config;
|
||||
}
|
||||
}
|
||||
|
||||
public static CampusBehaviorConfig Load(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
GD.PushWarning("Campus behavior config path is empty; using defaults.");
|
||||
return new CampusBehaviorConfig();
|
||||
}
|
||||
|
||||
var resolvedPath = path.StartsWith("res://") || path.StartsWith("user://")
|
||||
? ProjectSettings.GlobalizePath(path)
|
||||
: path;
|
||||
|
||||
if (!File.Exists(resolvedPath))
|
||||
{
|
||||
GD.PushWarning($"Campus behavior config not found at {resolvedPath}; using defaults.");
|
||||
return new CampusBehaviorConfig();
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(resolvedPath);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
GD.PushWarning($"Campus behavior config is empty at {resolvedPath}; using defaults.");
|
||||
return new CampusBehaviorConfig();
|
||||
}
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
try
|
||||
{
|
||||
var config = JsonSerializer.Deserialize<CampusBehaviorConfig>(json, options);
|
||||
return config ?? new CampusBehaviorConfig();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PushWarning($"Failed to parse campus behavior config: {ex.Message}");
|
||||
return new CampusBehaviorConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple location registry that maps logical location ids to scene positions.
|
||||
/// This keeps the behavior system independent from scene tree details.
|
||||
/// </summary>
|
||||
public sealed class CampusLocationRegistry
|
||||
{
|
||||
private readonly Dictionary<CampusLocationId, Vector2> _locations = new();
|
||||
|
||||
public void Register(CampusLocationId id, Vector2 position)
|
||||
{
|
||||
if (id == CampusLocationId.None) return;
|
||||
_locations[id] = position;
|
||||
}
|
||||
|
||||
public bool TryGetPosition(CampusLocationId id, out Vector2 position)
|
||||
{
|
||||
return _locations.TryGetValue(id, out position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks current occupancy per location so traits like social phobia can react
|
||||
/// to crowd size without hard-coding scene knowledge.
|
||||
/// </summary>
|
||||
public sealed class CampusBehaviorWorld
|
||||
{
|
||||
private readonly Dictionary<CampusLocationId, int> _occupancy = new();
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_occupancy.Clear();
|
||||
}
|
||||
|
||||
public void AddOccupant(CampusLocationId id)
|
||||
{
|
||||
if (id == CampusLocationId.None || id == CampusLocationId.RandomWander) return;
|
||||
if (!_occupancy.ContainsKey(id))
|
||||
{
|
||||
_occupancy[id] = 0;
|
||||
}
|
||||
|
||||
_occupancy[id] += 1;
|
||||
}
|
||||
|
||||
public int GetOccupancy(CampusLocationId id)
|
||||
{
|
||||
return _occupancy.TryGetValue(id, out var count) ? count : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight task container for the campus demo; it just tracks remaining work.
|
||||
/// </summary>
|
||||
public sealed class CampusTask
|
||||
{
|
||||
public CampusTaskType Type { get; }
|
||||
public float RemainingSeconds { get; private set; }
|
||||
|
||||
public CampusTask(CampusTaskType type, float remainingSeconds)
|
||||
{
|
||||
Type = type;
|
||||
RemainingSeconds = Mathf.Max(0f, remainingSeconds);
|
||||
}
|
||||
|
||||
public void Advance(float delta)
|
||||
{
|
||||
RemainingSeconds = Mathf.Max(0f, RemainingSeconds - delta);
|
||||
}
|
||||
|
||||
public bool IsComplete => RemainingSeconds <= 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom needs that are not yet part of the core UnitModel (hunger/social/energy).
|
||||
/// Uses PropertyValue so it plugs into the existing numeric system.
|
||||
/// </summary>
|
||||
public sealed class CampusAgentNeeds
|
||||
{
|
||||
public PropertyValue Hunger { get; }
|
||||
public PropertyValue Energy { get; }
|
||||
public PropertyValue Social { get; }
|
||||
public PropertyValue Health { get; }
|
||||
|
||||
public CampusAgentNeeds(float hunger, float energy, float social, float health)
|
||||
{
|
||||
Hunger = new PropertyValue(hunger);
|
||||
Energy = new PropertyValue(energy);
|
||||
Social = new PropertyValue(social);
|
||||
Health = new PropertyValue(health);
|
||||
}
|
||||
}
|
||||
1
scripts/Campus/CampusBehaviorConfig.cs.uid
Normal file
1
scripts/Campus/CampusBehaviorConfig.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dbp5g1aqtlqhi
|
||||
@ -24,6 +24,9 @@ public partial class CampusStudent : CharacterBody2D
|
||||
private Rid _navigationMap;
|
||||
private Vector2 _currentTarget = Vector2.Zero;
|
||||
private bool _hasTarget;
|
||||
private bool _behaviorControlEnabled;
|
||||
private Vector2 _behaviorTarget = Vector2.Zero;
|
||||
private bool _behaviorHasTarget;
|
||||
private bool _usePhysicsMovement = true;
|
||||
[Export] public float MoveSpeed { get; set; } = 60.0f;
|
||||
[Export] public float TargetReachDistance { get; set; } = 6.0f;
|
||||
@ -66,7 +69,7 @@ public partial class CampusStudent : CharacterBody2D
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
_lastDelta = delta;
|
||||
if (_navigationAgent == null || _patrolPoints.Count == 0)
|
||||
if (_navigationAgent == null || (!_behaviorControlEnabled && _patrolPoints.Count == 0) || (_behaviorControlEnabled && !_behaviorHasTarget))
|
||||
{
|
||||
PlayIdleAnimation();
|
||||
return;
|
||||
@ -74,7 +77,22 @@ public partial class CampusStudent : CharacterBody2D
|
||||
|
||||
// 到达目标点或无路可走时,切换到下一个巡游点
|
||||
if (_navigationAgent.IsNavigationFinished())
|
||||
{
|
||||
if (_behaviorControlEnabled)
|
||||
{
|
||||
Velocity = Vector2.Zero;
|
||||
if (!EnableAvoidance && _usePhysicsMovement)
|
||||
{
|
||||
MoveAndSlide();
|
||||
}
|
||||
|
||||
PlayIdleAnimation();
|
||||
UpdateStuckTimer(delta);
|
||||
return;
|
||||
}
|
||||
|
||||
AdvanceTarget();
|
||||
}
|
||||
|
||||
var nextPosition = _navigationAgent.GetNextPathPosition();
|
||||
var toNext = nextPosition - GlobalPosition;
|
||||
@ -121,6 +139,43 @@ public partial class CampusStudent : CharacterBody2D
|
||||
if (_navigationAgent != null) AdvanceTarget();
|
||||
}
|
||||
|
||||
public void EnableBehaviorControl()
|
||||
{
|
||||
_behaviorControlEnabled = true;
|
||||
_behaviorHasTarget = false;
|
||||
_patrolConfigured = false;
|
||||
_patrolPoints.Clear();
|
||||
}
|
||||
|
||||
public void SetBehaviorTarget(Vector2 target)
|
||||
{
|
||||
_behaviorControlEnabled = true;
|
||||
_behaviorTarget = target;
|
||||
_behaviorHasTarget = true;
|
||||
_currentTarget = target;
|
||||
_hasTarget = true;
|
||||
_stuckTimer = 0.0f;
|
||||
|
||||
if (_navigationAgent != null)
|
||||
{
|
||||
_navigationAgent.TargetPosition = target;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearBehaviorTarget()
|
||||
{
|
||||
_behaviorHasTarget = false;
|
||||
_hasTarget = false;
|
||||
_currentTarget = Vector2.Zero;
|
||||
_stuckTimer = 0.0f;
|
||||
}
|
||||
|
||||
public bool HasReachedBehaviorTarget()
|
||||
{
|
||||
if (!_behaviorHasTarget || _navigationAgent == null) return true;
|
||||
return _navigationAgent.IsNavigationFinished();
|
||||
}
|
||||
|
||||
public void SetNavigationMap(Rid map)
|
||||
{
|
||||
// 由校园控制器传入导航地图,供本地边界夹紧使用
|
||||
@ -163,6 +218,7 @@ public partial class CampusStudent : CharacterBody2D
|
||||
private void AdvanceTarget()
|
||||
{
|
||||
if (_patrolPoints.Count == 0 || _navigationAgent == null) return;
|
||||
if (_behaviorControlEnabled) return;
|
||||
|
||||
// 避免当前点过近导致原地抖动,最多尝试一轮
|
||||
for (var i = 0; i < _patrolPoints.Count; i++)
|
||||
|
||||
1
scripts/Core/ContentCollectionResource.cs.uid
Normal file
1
scripts/Core/ContentCollectionResource.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://ctbmcynvl8ffm
|
||||
1
scripts/Core/ContentRegistry.cs.uid
Normal file
1
scripts/Core/ContentRegistry.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://fpwckd80v5nv
|
||||
1
scripts/Core/ContentResources.cs.uid
Normal file
1
scripts/Core/ContentResources.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bkgwb8oeer5ri
|
||||
1
scripts/Core/DisciplineDefinitionResource.cs.uid
Normal file
1
scripts/Core/DisciplineDefinitionResource.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bfehg7ijcybie
|
||||
1
scripts/Core/DomainEvents.cs.uid
Normal file
1
scripts/Core/DomainEvents.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://drbjfxehidfrn
|
||||
1
scripts/Core/EventBus.cs.uid
Normal file
1
scripts/Core/EventBus.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dmhvn0hu7qlne
|
||||
1
scripts/Core/GameController.cs.uid
Normal file
1
scripts/Core/GameController.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cqw1q6qv873he
|
||||
@ -40,6 +40,8 @@ public sealed class GameSession
|
||||
var jsonSource = new JsonContentSource(10);
|
||||
jsonSource.DataPaths.Add("res://resources/definitions/disciplines.json");
|
||||
jsonSource.DataPaths.Add("res://resources/definitions/archetypes.json");
|
||||
jsonSource.DataPaths.Add("res://resources/definitions/roles.json");
|
||||
jsonSource.DataPaths.Add("res://resources/definitions/traits.json");
|
||||
registry.RegisterSource(jsonSource);
|
||||
|
||||
var content = registry.BuildDatabase();
|
||||
|
||||
1
scripts/Core/GameSession.cs.uid
Normal file
1
scripts/Core/GameSession.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cltmymdid63wi
|
||||
1
scripts/Core/GameSystems.cs.uid
Normal file
1
scripts/Core/GameSystems.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dg4tya2t3wgel
|
||||
1
scripts/Core/LocalizationService.cs.uid
Normal file
1
scripts/Core/LocalizationService.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bi61o586eqdyu
|
||||
1
scripts/Core/ModManifest.cs.uid
Normal file
1
scripts/Core/ModManifest.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c7ng0xpd2iul1
|
||||
1
scripts/Core/Mvc.cs.uid
Normal file
1
scripts/Core/Mvc.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bqconwrqysw5b
|
||||
1
scripts/Core/StatResolver.cs.uid
Normal file
1
scripts/Core/StatResolver.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://l1exgbqpbayh
|
||||
1
scripts/Models/CoreIds.cs.uid
Normal file
1
scripts/Models/CoreIds.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b8dobhu11y8kt
|
||||
1
scripts/Models/DefinitionSupport.cs.uid
Normal file
1
scripts/Models/DefinitionSupport.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bpvgbmxh1gq6u
|
||||
1
scripts/Models/DisciplineDefinitions.cs.uid
Normal file
1
scripts/Models/DisciplineDefinitions.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cdec5vh0h2md
|
||||
1
scripts/Models/DomainEnums.cs.uid
Normal file
1
scripts/Models/DomainEnums.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dco1nttd0pgqp
|
||||
1
scripts/Models/GameContentDatabase.cs.uid
Normal file
1
scripts/Models/GameContentDatabase.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://i6eakpcujm0q
|
||||
1
scripts/Models/GameState.cs.uid
Normal file
1
scripts/Models/GameState.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bm4fbpk2hmxkj
|
||||
1
scripts/Models/ItemDefinitions.cs.uid
Normal file
1
scripts/Models/ItemDefinitions.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://buicivgjyfews
|
||||
1
scripts/Models/Modifiers.cs.uid
Normal file
1
scripts/Models/Modifiers.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bpjfyyhudrq8b
|
||||
1
scripts/Models/PaperDefinitions.cs.uid
Normal file
1
scripts/Models/PaperDefinitions.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://mtm1ypht0ynd
|
||||
1
scripts/Models/RogueliteDefinitions.cs.uid
Normal file
1
scripts/Models/RogueliteDefinitions.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cjcfah7uglbad
|
||||
1
scripts/Models/StaffModel.cs.uid
Normal file
1
scripts/Models/StaffModel.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cx1jd2ydj5nof
|
||||
1
scripts/Models/SynergyDefinitions.cs.uid
Normal file
1
scripts/Models/SynergyDefinitions.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://vqos5kin02p1
|
||||
1
scripts/Models/TaskDefinitions.cs.uid
Normal file
1
scripts/Models/TaskDefinitions.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bth3oambl7pnk
|
||||
1
scripts/Models/UnitComponents.cs.uid
Normal file
1
scripts/Models/UnitComponents.cs.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://huerc661rxe7
|
||||
@ -6,14 +6,19 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="docs\design.md" />
|
||||
<Content Include="docs\校园行为系统文档.md" />
|
||||
<Content Include="docs\装备与设施系统.md" />
|
||||
<Content Include="docs\学科与流派系统.md" />
|
||||
<Content Include="docs\任务与经济系统.md" />
|
||||
<Content Include="docs\角色与羁绊系统.md" />
|
||||
<Content Include="docs\角色与行为规则.md" />
|
||||
<Content Include="README.md" />
|
||||
<Content Include="resources\definitions\archetypes.json" />
|
||||
<Content Include="resources\definitions\campus_behavior.json" />
|
||||
<Content Include="resources\definitions\disciplines.json" />
|
||||
<Content Include="resources\definitions\discipline_biology.tres" />
|
||||
<Content Include="resources\definitions\roles.json" />
|
||||
<Content Include="resources\definitions\traits.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="addons\" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user