diff --git a/docs/design.md b/docs/design.md index 91f7248..3b78f37 100644 --- a/docs/design.md +++ b/docs/design.md @@ -171,5 +171,3 @@ * **策略性:** 不只是堆数值,而是在“做学术”和“搞政治”之间做平衡。 * **情感反馈:** 从初期的无助,到中期的纠结,再到后期的“屠龙少年终成恶龙”或“桃李满天下”的感动。 -=== - diff --git a/docs/校园行为系统文档.md b/docs/校园行为系统文档.md new file mode 100644 index 0000000..61e7d3b --- /dev/null +++ b/docs/校园行为系统文档.md @@ -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` 基类。 +- `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()`。 diff --git a/docs/角色与行为规则.md b/docs/角色与行为规则.md new file mode 100644 index 0000000..e394e8e --- /dev/null +++ b/docs/角色与行为规则.md @@ -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。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5bbb9c5 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b55cf70 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "yargs-parser": "^22.0.0" + } +} diff --git a/resources/definitions/archetypes.json b/resources/definitions/archetypes.json index 52d0573..f678294 100644 --- a/resources/definitions/archetypes.json +++ b/resources/definitions/archetypes.json @@ -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" ] } } ] diff --git a/resources/definitions/campus_behavior.json b/resources/definitions/campus_behavior.json new file mode 100644 index 0000000..a5e09c0 --- /dev/null +++ b/resources/definitions/campus_behavior.json @@ -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 + } + ] +} diff --git a/resources/definitions/roles.json b/resources/definitions/roles.json new file mode 100644 index 0000000..59f8836 --- /dev/null +++ b/resources/definitions/roles.json @@ -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" + ] + } +] diff --git a/resources/definitions/traits.json b/resources/definitions/traits.json new file mode 100644 index 0000000..c222627 --- /dev/null +++ b/resources/definitions/traits.json @@ -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" ] + } + } +] diff --git a/scenes/CampusController.cs b/scenes/CampusController.cs index 160b05b..cd44a29 100644 --- a/scenes/CampusController.cs +++ b/scenes/CampusController.cs @@ -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 _coveragePoints = new(); + private readonly List _behaviorAgents = new(); + private readonly CampusBehaviorWorld _behaviorWorld = new(); + private readonly CampusLocationRegistry _locationRegistry = new(); + private CampusBehaviorConfig _behaviorConfig; + private GameContentDatabase _contentDatabase; + private List _archetypeIds = new(); + private List _roleIds = new(); + private List _traitIds = new(); + private List _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("Sprite2D/NavigationRegion2D"); _studentsRoot = GetNodeOrNull("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(_contentDatabase.Archetypes.Keys); + _roleIds = new List(_contentDatabase.Roles.Keys); + _traitIds = new List(_contentDatabase.Traits.Keys); + _disciplineIds = new List(_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("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(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 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 BuildCoveragePoints() { var points = new List(); diff --git a/scenes/campus.tscn b/scenes/campus.tscn index 9c262bf..591682a 100644 --- a/scenes/campus.tscn +++ b/scenes/campus.tscn @@ -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) diff --git a/scripts/Campus/CampusBehaviorAgent.cs b/scripts/Campus/CampusBehaviorAgent.cs new file mode 100644 index 0000000..c41c27e --- /dev/null +++ b/scripts/Campus/CampusBehaviorAgent.cs @@ -0,0 +1,778 @@ +using System; +using System.Collections.Generic; +using Godot; +using Models; + +/// +/// Runtime data for a campus agent. This keeps Godot nodes and pure data separate +/// so the behavior system can be tested without scene dependencies. +/// +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); +} + +/// +/// 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. +/// +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; + } +} + +/// +/// Shared context passed into providers/states so they can evaluate the same data +/// without hard-coding dependencies. +/// +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 WanderPoints { get; } + + public CampusBehaviorContext( + CampusAgentRuntime agent, + CampusBehaviorConfig config, + CampusLocationRegistry locations, + CampusBehaviorWorld world, + Random rng, + List wanderPoints) + { + Agent = agent; + Config = config; + Locations = locations; + World = world; + Rng = rng; + WanderPoints = wanderPoints; + } +} + +/// +/// 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. +/// +public interface ICampusBehaviorProvider +{ + CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context); +} + +/// +/// Critical state provider: handles sanity collapse, extreme stress, or exhaustion. +/// This is the highest priority in the decision queue. +/// +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; + } +} + +/// +/// Assigned task provider: if the agent has a task, it is executed before needs. +/// +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 + }; + } +} + +/// +/// Needs provider: hunger, fatigue, mood, and social needs are handled here. +/// It sits below assigned tasks but above trait-driven idle behavior. +/// +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; + } +} + +/// +/// Trait-driven provider: applies long-term personality or tag tendencies when +/// there is no urgent need. +/// +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; + } +} + +/// +/// Idle provider: default fallback when nothing else applies. +/// +public sealed class IdleBehaviorProvider : ICampusBehaviorProvider +{ + public CampusBehaviorIntent TryCreateIntent(CampusBehaviorContext context) + { + return new CampusBehaviorIntent( + CampusBehaviorPriority.Idle, + CampusActionId.Wandering, + CampusLocationId.RandomWander, + "idle_wander"); + } +} + +/// +/// Planner executes providers in priority order. This lets us add or remove +/// providers without editing the state machine. +/// +public sealed class CampusBehaviorPlanner +{ + private readonly List _providers; + + public CampusBehaviorPlanner(IEnumerable providers) + { + _providers = new List(providers ?? Array.Empty()); + } + + 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"); + } +} + +/// +/// State interface for the AI FSM. Each state can transition by requesting +/// a change via the owning behavior agent. +/// +public interface ICampusBehaviorState +{ + void Enter(CampusBehaviorAgent agent); + void Tick(CampusBehaviorAgent agent, float delta); + void Exit(CampusBehaviorAgent agent); +} + +/// +/// State machine wrapper to enforce enter/exit semantics. +/// +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); + } +} + +/// +/// Decision state: pick a new intent and immediately transition to movement. +/// This keeps the intent selection isolated and easy to extend. +/// +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) + { + } +} + +/// +/// Movement state: navigate to the intent's target location. +/// Once the agent arrives, it transitions into the action state. +/// +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) + { + } +} + +/// +/// Action state: apply per-second deltas and update task progress. +/// When the action duration expires, transition back to decision. +/// +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; + } + } +} + +/// +/// Main behavior agent that drives one campus character. It owns the planner, +/// state machine, and applies baseline stat decay on every tick. +/// +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 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()); + _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 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(); + } +} + +/// +/// Centralized IDs for traits referenced by the behavior system. +/// Keeping them here avoids scattering magic strings. +/// +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"; +} diff --git a/scripts/Campus/CampusBehaviorAgent.cs.uid b/scripts/Campus/CampusBehaviorAgent.cs.uid new file mode 100644 index 0000000..3562c59 --- /dev/null +++ b/scripts/Campus/CampusBehaviorAgent.cs.uid @@ -0,0 +1 @@ +uid://bd1bjq6vrs4hh diff --git a/scripts/Campus/CampusBehaviorConfig.cs b/scripts/Campus/CampusBehaviorConfig.cs new file mode 100644 index 0000000..9bb1448 --- /dev/null +++ b/scripts/Campus/CampusBehaviorConfig.cs @@ -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; + +/// +/// Location identifiers used by the campus behavior system. +/// These map to Node2D markers in campus.tscn so the AI can pick targets by name. +/// +public enum CampusLocationId +{ + None, + Laboratory, + Library, + Canteen, + Dormitory, + ArtificialLake, + CoffeeShop, + AdministrationBuilding, + FootballField, + RandomWander +} + +/// +/// Action identifiers used by the behavior planner and state machine. +/// Each action is configured via campus_behavior.json for duration and stat deltas. +/// +public enum CampusActionId +{ + None, + Experimenting, + Writing, + Eating, + Sleeping, + Chilling, + Staring, + CoffeeBreak, + Administration, + Running, + Socializing, + Wandering +} + +/// +/// Priority levels match the design doc ordering: lower value = higher priority. +/// +public enum CampusBehaviorPriority +{ + Critical = 0, + AssignedTask = 1, + Needs = 2, + Trait = 3, + Idle = 4 +} + +/// +/// Minimal task types for the campus demo. These are not full gameplay tasks, +/// just drivers for the assigned-task priority in the AI. +/// +public enum CampusTaskType +{ + Experiment, + Writing, + Administration, + Exercise, + Coding, + Social +} + +/// +/// Action configuration loaded from JSON. Deltas are applied per second while +/// the action is running, so longer actions accumulate more effect. +/// +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; } +} + +/// +/// Global behavior configuration for campus AI. This is intentionally data-driven +/// so balancing can happen in JSON without touching code. +/// +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 ActionConfigs { get; set; } = new(); + + private readonly Dictionary _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(json, options); + return config ?? new CampusBehaviorConfig(); + } + catch (Exception ex) + { + GD.PushWarning($"Failed to parse campus behavior config: {ex.Message}"); + return new CampusBehaviorConfig(); + } + } +} + +/// +/// Simple location registry that maps logical location ids to scene positions. +/// This keeps the behavior system independent from scene tree details. +/// +public sealed class CampusLocationRegistry +{ + private readonly Dictionary _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); + } +} + +/// +/// Tracks current occupancy per location so traits like social phobia can react +/// to crowd size without hard-coding scene knowledge. +/// +public sealed class CampusBehaviorWorld +{ + private readonly Dictionary _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; + } +} + +/// +/// Lightweight task container for the campus demo; it just tracks remaining work. +/// +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; +} + +/// +/// Custom needs that are not yet part of the core UnitModel (hunger/social/energy). +/// Uses PropertyValue so it plugs into the existing numeric system. +/// +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); + } +} diff --git a/scripts/Campus/CampusBehaviorConfig.cs.uid b/scripts/Campus/CampusBehaviorConfig.cs.uid new file mode 100644 index 0000000..d2de741 --- /dev/null +++ b/scripts/Campus/CampusBehaviorConfig.cs.uid @@ -0,0 +1 @@ +uid://dbp5g1aqtlqhi diff --git a/scripts/CampusStudent.cs b/scripts/CampusStudent.cs index e47a61e..c0753c5 100644 --- a/scripts/CampusStudent.cs +++ b/scripts/CampusStudent.cs @@ -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++) diff --git a/scripts/Core/ContentCollectionResource.cs.uid b/scripts/Core/ContentCollectionResource.cs.uid new file mode 100644 index 0000000..0b84392 --- /dev/null +++ b/scripts/Core/ContentCollectionResource.cs.uid @@ -0,0 +1 @@ +uid://ctbmcynvl8ffm diff --git a/scripts/Core/ContentRegistry.cs.uid b/scripts/Core/ContentRegistry.cs.uid new file mode 100644 index 0000000..5fbc9d4 --- /dev/null +++ b/scripts/Core/ContentRegistry.cs.uid @@ -0,0 +1 @@ +uid://fpwckd80v5nv diff --git a/scripts/Core/ContentResources.cs.uid b/scripts/Core/ContentResources.cs.uid new file mode 100644 index 0000000..1f8c255 --- /dev/null +++ b/scripts/Core/ContentResources.cs.uid @@ -0,0 +1 @@ +uid://bkgwb8oeer5ri diff --git a/scripts/Core/DisciplineDefinitionResource.cs.uid b/scripts/Core/DisciplineDefinitionResource.cs.uid new file mode 100644 index 0000000..17c630d --- /dev/null +++ b/scripts/Core/DisciplineDefinitionResource.cs.uid @@ -0,0 +1 @@ +uid://bfehg7ijcybie diff --git a/scripts/Core/DomainEvents.cs.uid b/scripts/Core/DomainEvents.cs.uid new file mode 100644 index 0000000..897adc4 --- /dev/null +++ b/scripts/Core/DomainEvents.cs.uid @@ -0,0 +1 @@ +uid://drbjfxehidfrn diff --git a/scripts/Core/EventBus.cs.uid b/scripts/Core/EventBus.cs.uid new file mode 100644 index 0000000..951b1f2 --- /dev/null +++ b/scripts/Core/EventBus.cs.uid @@ -0,0 +1 @@ +uid://dmhvn0hu7qlne diff --git a/scripts/Core/GameController.cs.uid b/scripts/Core/GameController.cs.uid new file mode 100644 index 0000000..c572176 --- /dev/null +++ b/scripts/Core/GameController.cs.uid @@ -0,0 +1 @@ +uid://cqw1q6qv873he diff --git a/scripts/Core/GameSession.cs b/scripts/Core/GameSession.cs index 9840d00..4a12208 100644 --- a/scripts/Core/GameSession.cs +++ b/scripts/Core/GameSession.cs @@ -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(); diff --git a/scripts/Core/GameSession.cs.uid b/scripts/Core/GameSession.cs.uid new file mode 100644 index 0000000..8f54626 --- /dev/null +++ b/scripts/Core/GameSession.cs.uid @@ -0,0 +1 @@ +uid://cltmymdid63wi diff --git a/scripts/Core/GameSystems.cs.uid b/scripts/Core/GameSystems.cs.uid new file mode 100644 index 0000000..a84c294 --- /dev/null +++ b/scripts/Core/GameSystems.cs.uid @@ -0,0 +1 @@ +uid://dg4tya2t3wgel diff --git a/scripts/Core/LocalizationService.cs.uid b/scripts/Core/LocalizationService.cs.uid new file mode 100644 index 0000000..fb06ab6 --- /dev/null +++ b/scripts/Core/LocalizationService.cs.uid @@ -0,0 +1 @@ +uid://bi61o586eqdyu diff --git a/scripts/Core/ModManifest.cs.uid b/scripts/Core/ModManifest.cs.uid new file mode 100644 index 0000000..ed87cd6 --- /dev/null +++ b/scripts/Core/ModManifest.cs.uid @@ -0,0 +1 @@ +uid://c7ng0xpd2iul1 diff --git a/scripts/Core/Mvc.cs.uid b/scripts/Core/Mvc.cs.uid new file mode 100644 index 0000000..e5af8ac --- /dev/null +++ b/scripts/Core/Mvc.cs.uid @@ -0,0 +1 @@ +uid://bqconwrqysw5b diff --git a/scripts/Core/StatResolver.cs.uid b/scripts/Core/StatResolver.cs.uid new file mode 100644 index 0000000..d583d73 --- /dev/null +++ b/scripts/Core/StatResolver.cs.uid @@ -0,0 +1 @@ +uid://l1exgbqpbayh diff --git a/scripts/Models/CoreIds.cs.uid b/scripts/Models/CoreIds.cs.uid new file mode 100644 index 0000000..3fbdb4f --- /dev/null +++ b/scripts/Models/CoreIds.cs.uid @@ -0,0 +1 @@ +uid://b8dobhu11y8kt diff --git a/scripts/Models/DefinitionSupport.cs.uid b/scripts/Models/DefinitionSupport.cs.uid new file mode 100644 index 0000000..c9b969d --- /dev/null +++ b/scripts/Models/DefinitionSupport.cs.uid @@ -0,0 +1 @@ +uid://bpvgbmxh1gq6u diff --git a/scripts/Models/DisciplineDefinitions.cs.uid b/scripts/Models/DisciplineDefinitions.cs.uid new file mode 100644 index 0000000..c4ed793 --- /dev/null +++ b/scripts/Models/DisciplineDefinitions.cs.uid @@ -0,0 +1 @@ +uid://cdec5vh0h2md diff --git a/scripts/Models/DomainEnums.cs.uid b/scripts/Models/DomainEnums.cs.uid new file mode 100644 index 0000000..871730d --- /dev/null +++ b/scripts/Models/DomainEnums.cs.uid @@ -0,0 +1 @@ +uid://dco1nttd0pgqp diff --git a/scripts/Models/GameContentDatabase.cs.uid b/scripts/Models/GameContentDatabase.cs.uid new file mode 100644 index 0000000..820cdcb --- /dev/null +++ b/scripts/Models/GameContentDatabase.cs.uid @@ -0,0 +1 @@ +uid://i6eakpcujm0q diff --git a/scripts/Models/GameState.cs.uid b/scripts/Models/GameState.cs.uid new file mode 100644 index 0000000..8c46eff --- /dev/null +++ b/scripts/Models/GameState.cs.uid @@ -0,0 +1 @@ +uid://bm4fbpk2hmxkj diff --git a/scripts/Models/ItemDefinitions.cs.uid b/scripts/Models/ItemDefinitions.cs.uid new file mode 100644 index 0000000..e2f7207 --- /dev/null +++ b/scripts/Models/ItemDefinitions.cs.uid @@ -0,0 +1 @@ +uid://buicivgjyfews diff --git a/scripts/Models/Modifiers.cs.uid b/scripts/Models/Modifiers.cs.uid new file mode 100644 index 0000000..db095a8 --- /dev/null +++ b/scripts/Models/Modifiers.cs.uid @@ -0,0 +1 @@ +uid://bpjfyyhudrq8b diff --git a/scripts/Models/PaperDefinitions.cs.uid b/scripts/Models/PaperDefinitions.cs.uid new file mode 100644 index 0000000..16365d3 --- /dev/null +++ b/scripts/Models/PaperDefinitions.cs.uid @@ -0,0 +1 @@ +uid://mtm1ypht0ynd diff --git a/scripts/Models/RogueliteDefinitions.cs.uid b/scripts/Models/RogueliteDefinitions.cs.uid new file mode 100644 index 0000000..a6acbea --- /dev/null +++ b/scripts/Models/RogueliteDefinitions.cs.uid @@ -0,0 +1 @@ +uid://cjcfah7uglbad diff --git a/scripts/Models/StaffModel.cs.uid b/scripts/Models/StaffModel.cs.uid new file mode 100644 index 0000000..94b9b5b --- /dev/null +++ b/scripts/Models/StaffModel.cs.uid @@ -0,0 +1 @@ +uid://cx1jd2ydj5nof diff --git a/scripts/Models/SynergyDefinitions.cs.uid b/scripts/Models/SynergyDefinitions.cs.uid new file mode 100644 index 0000000..f0124f0 --- /dev/null +++ b/scripts/Models/SynergyDefinitions.cs.uid @@ -0,0 +1 @@ +uid://vqos5kin02p1 diff --git a/scripts/Models/TaskDefinitions.cs.uid b/scripts/Models/TaskDefinitions.cs.uid new file mode 100644 index 0000000..30f4863 --- /dev/null +++ b/scripts/Models/TaskDefinitions.cs.uid @@ -0,0 +1 @@ +uid://bth3oambl7pnk diff --git a/scripts/Models/UnitComponents.cs.uid b/scripts/Models/UnitComponents.cs.uid new file mode 100644 index 0000000..57a93f9 --- /dev/null +++ b/scripts/Models/UnitComponents.cs.uid @@ -0,0 +1 @@ +uid://huerc661rxe7 diff --git a/最强导师.csproj b/最强导师.csproj index 00ed44d..e108a27 100644 --- a/最强导师.csproj +++ b/最强导师.csproj @@ -6,14 +6,19 @@ + + + + +