diff --git a/scripts/Core/DomainEvents.cs b/scripts/Core/DomainEvents.cs
new file mode 100644
index 0000000..4ac4547
--- /dev/null
+++ b/scripts/Core/DomainEvents.cs
@@ -0,0 +1,44 @@
+using Models;
+
+namespace Core;
+
+///
+/// 领域事件(用于系统解耦)
+/// 设计说明:
+/// 1) Task/Economy/Turn 等系统通过事件通信,避免直接依赖。
+/// 2) 事件只携带最小必要信息,避免模型被过度暴露。
+/// 注意事项:
+/// - 事件是同步派发,请避免在处理器中做耗时操作。
+/// 未来扩展:
+/// - 可加入“事件上下文/来源系统”等字段,便于调试。
+///
+public readonly struct TaskCompletedEvent
+{
+ public TaskModel Task { get; }
+
+ public TaskCompletedEvent(TaskModel task)
+ {
+ Task = task;
+ }
+}
+
+public readonly struct TaskFailedEvent
+{
+ public TaskModel Task { get; }
+
+ public TaskFailedEvent(TaskModel task)
+ {
+ Task = task;
+ }
+}
+
+public readonly struct TurnEndedEvent
+{
+ public int Turn { get; }
+
+ public TurnEndedEvent(int turn)
+ {
+ Turn = turn;
+ }
+}
+
diff --git a/scripts/Core/GameSystems.cs b/scripts/Core/GameSystems.cs
index 81a1374..3e69452 100644
--- a/scripts/Core/GameSystems.cs
+++ b/scripts/Core/GameSystems.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Collections.Generic;
using Models;
namespace Core;
@@ -38,9 +40,9 @@ public sealed class GameSystems
public void Tick(float delta)
{
Turn.Tick(delta);
+ Synergy.Tick(delta);
Task.Tick(delta);
Economy.Tick(delta);
- Synergy.Tick(delta);
Assignment.Tick(delta);
}
}
@@ -63,30 +65,430 @@ public sealed class TurnSystem : IGameSystem
public sealed class TaskSystem : IGameSystem
{
private GameSession _session;
+ private StatResolver _statResolver;
public void Initialize(GameSession session)
{
_session = session;
+ _statResolver = new StatResolver(session);
}
public void Tick(float delta)
{
- // 预留:执行阶段推进任务进度
+ if (_session.State.Turn.Phase != GamePhase.Execution)
+ {
+ return;
+ }
+
+ AdvanceTasks(delta);
+ }
+
+ ///
+ /// 回合结算(扣除 Deadline,判定完成与失败)
+ ///
+ public void ResolveEndOfTurn()
+ {
+ var state = _session.State;
+ var completed = new List();
+ var failed = new List();
+
+ foreach (var task in state.Tasks.ActiveTasks)
+ {
+ if (task.Runtime.RemainingTurns > 0)
+ {
+ task.Runtime.RemainingTurns--;
+ }
+
+ if (task.IsCompleted)
+ {
+ completed.Add(task);
+ }
+ else if (task.IsFailed)
+ {
+ failed.Add(task);
+ }
+ }
+
+ foreach (var task in completed)
+ {
+ state.Tasks.ActiveTasks.Remove(task);
+ state.Tasks.CompletedTasks.Add(task);
+ _session.Events.Publish(new TaskCompletedEvent(task));
+ }
+
+ foreach (var task in failed)
+ {
+ state.Tasks.ActiveTasks.Remove(task);
+ state.Tasks.FailedTasks.Add(task);
+ _session.Events.Publish(new TaskFailedEvent(task));
+ }
+ }
+
+ private void AdvanceTasks(float delta)
+ {
+ var state = _session.State;
+ if (state.Tasks.ActiveTasks.Count == 0) return;
+
+ var unitIndex = BuildUnitIndex();
+
+ foreach (var task in state.Tasks.ActiveTasks)
+ {
+ if (task.IsCompleted || task.IsFailed) continue;
+ if (task.Kind == TaskKind.Milestone) continue;
+
+ var taskDef = GetTaskDefinition(task);
+ var totalPower = 0f;
+
+ foreach (var unitId in task.AssignedUnitIds)
+ {
+ if (!unitIndex.TryGetValue(unitId, out var entry))
+ {
+ continue;
+ }
+
+ var contribution = GetUnitContribution(entry, task, taskDef, delta);
+ totalPower += contribution;
+ }
+
+ if (totalPower <= 0f) continue;
+
+ var difficultyScale = task.Runtime.DifficultyScale * GetDifficultyScale(task.Difficulty);
+ task.AddProgress(totalPower * delta / difficultyScale);
+ }
+ }
+
+ private float GetUnitContribution(UnitEntry entry, TaskModel task, TaskDefinition taskDef, float delta)
+ {
+ var unit = entry.Unit;
+ var basePower = GetTaskBasePower(unit, task.Kind);
+ if (basePower <= 0f) return 0f;
+
+ var roleMultiplier = GetRoleMultiplier(unit, taskDef);
+ var disciplineMultiplier = GetDisciplineMultiplier(unit, taskDef);
+ var statusMultiplier = GetStatusMultiplier(entry);
+ var requirementMultiplier = GetRequirementMultiplier(unit, taskDef);
+
+ var effectivePower = basePower * roleMultiplier * disciplineMultiplier * statusMultiplier * requirementMultiplier;
+ TrackContribution(entry, task, effectivePower * delta);
+ return effectivePower;
+ }
+
+ private float GetTaskBasePower(UnitModel unit, TaskKind kind)
+ {
+ var academic = _statResolver.GetAttribute(unit, AttributeType.Academic);
+ var engineering = _statResolver.GetAttribute(unit, AttributeType.Engineering);
+ var writing = _statResolver.GetAttribute(unit, AttributeType.Writing);
+ var financial = _statResolver.GetAttribute(unit, AttributeType.Financial);
+ var social = _statResolver.GetAttribute(unit, AttributeType.Social);
+ var activation = _statResolver.GetAttribute(unit, AttributeType.Activation);
+
+ return kind switch
+ {
+ TaskKind.AcademicExploration => academic * 0.6f + writing * 0.4f,
+ TaskKind.GrantVertical => writing * 0.4f + social * 0.4f + financial * 0.2f,
+ TaskKind.GrantHorizontal => engineering * 0.5f + activation * 0.3f + academic * 0.2f,
+ TaskKind.Administrative => activation * 0.6f + social * 0.4f,
+ TaskKind.Conference => social * 0.5f + writing * 0.3f + financial * 0.2f,
+ TaskKind.Milestone => 0f,
+ _ => academic
+ };
+ }
+
+ private float GetStatusMultiplier(UnitEntry entry)
+ {
+ var mood = entry.Unit.Statuses.Mood.Normalized;
+ var stress = entry.Unit.Statuses.Stress.Current.Normalized;
+
+ var moodMultiplier = 0.5f + mood * 0.5f;
+ var stressMultiplier = Math.Clamp(1.0f - 0.3f * stress, 0.7f, 1.0f);
+ var multiplier = moodMultiplier * stressMultiplier;
+
+ if (entry.Student != null)
+ {
+ var stamina = entry.Student.Progress.Stamina.Current.Normalized;
+ if (stamina < 0.1f) multiplier *= 0.4f;
+ else if (stamina < 0.2f) multiplier *= 0.7f;
+ }
+
+ return Math.Clamp(multiplier, 0.3f, 1.2f);
+ }
+
+ private float GetRoleMultiplier(UnitModel unit, TaskDefinition taskDef)
+ {
+ if (taskDef == null) return 1.0f;
+
+ if (taskDef.Requirements.RequiredRoleIds.Count > 0)
+ {
+ foreach (var roleId in taskDef.Requirements.RequiredRoleIds)
+ {
+ if (unit.Tags.RoleIds.Contains(roleId))
+ {
+ return 1.0f;
+ }
+ }
+
+ return 0.5f;
+ }
+
+ if (taskDef.RecommendedRoleIds.Count > 0)
+ {
+ foreach (var roleId in taskDef.RecommendedRoleIds)
+ {
+ if (unit.Tags.RoleIds.Contains(roleId))
+ {
+ return 1.1f;
+ }
+ }
+
+ return 0.9f;
+ }
+
+ return 1.0f;
+ }
+
+ private float GetRequirementMultiplier(UnitModel unit, TaskDefinition taskDef)
+ {
+ if (taskDef == null) return 1.0f;
+ var multiplier = 1.0f;
+
+ foreach (var requirement in taskDef.Requirements.AttributeChecks)
+ {
+ var value = _statResolver.GetAttribute(unit, requirement.Type);
+ if (value < requirement.MinValue)
+ {
+ multiplier *= 0.85f;
+ }
+ }
+
+ return multiplier;
+ }
+
+ private float GetDisciplineMultiplier(UnitModel unit, TaskDefinition taskDef)
+ {
+ if (taskDef == null) return 1.0f;
+ if (taskDef.AllowedDisciplineIds.Count == 0) return 1.0f;
+ if (string.IsNullOrWhiteSpace(unit.Tags.DisciplineId)) return 0.8f;
+
+ foreach (var disciplineId in taskDef.AllowedDisciplineIds)
+ {
+ if (disciplineId == unit.Tags.DisciplineId)
+ {
+ return 1.0f;
+ }
+ }
+
+ return 0.7f;
+ }
+
+ private float GetDifficultyScale(TaskDifficulty difficulty)
+ {
+ return difficulty switch
+ {
+ TaskDifficulty.Water => 0.8f,
+ TaskDifficulty.Standard => 1.0f,
+ TaskDifficulty.Hardcore => 1.3f,
+ TaskDifficulty.BlackBox => 1.6f,
+ _ => 1.0f
+ };
+ }
+
+ private TaskDefinition GetTaskDefinition(TaskModel task)
+ {
+ if (string.IsNullOrWhiteSpace(task.DefinitionId)) return null;
+ return _session.Content.Tasks.TryGetValue(task.DefinitionId, out var definition) ? definition : null;
+ }
+
+ private Dictionary BuildUnitIndex()
+ {
+ var index = new Dictionary();
+ var roster = _session.State.Roster;
+
+ if (roster.Mentor != null)
+ {
+ var unit = roster.Mentor.Core;
+ index[unit.Identity.Id] = new UnitEntry(unit, roster.Mentor, null, null);
+ }
+
+ foreach (var student in roster.Students)
+ {
+ var unit = student.Core;
+ index[unit.Identity.Id] = new UnitEntry(unit, null, student, null);
+ }
+
+ foreach (var staff in roster.Staffs)
+ {
+ var unit = staff.Core;
+ index[unit.Identity.Id] = new UnitEntry(unit, null, null, staff);
+ }
+
+ return index;
+ }
+
+ private void TrackContribution(UnitEntry entry, TaskModel task, float deltaContribution)
+ {
+ if (entry.Student == null) return;
+
+ var contributions = entry.Student.Contributions.ByTask;
+ if (!contributions.TryGetValue(task.Id, out var value))
+ {
+ value = new PropertyValue(0, 0, 1000000);
+ contributions[task.Id] = value;
+ }
+
+ value.Add(deltaContribution);
+ }
+
+ private readonly struct UnitEntry
+ {
+ public UnitModel Unit { get; }
+ public MentorModel Mentor { get; }
+ public StudentModel Student { get; }
+ public StaffModel Staff { get; }
+
+ public UnitEntry(UnitModel unit, MentorModel mentor, StudentModel student, StaffModel staff)
+ {
+ Unit = unit;
+ Mentor = mentor;
+ Student = student;
+ Staff = staff;
+ }
}
}
public sealed class EconomySystem : IGameSystem
{
private GameSession _session;
+ private const int MasterSalary = 500;
+ private const int DoctorSalary = 800;
+ private const int PostDocSalary = 1200;
+ private const int JuniorFacultySalary = 2000;
public void Initialize(GameSession session)
{
_session = session;
+ _session.Events.Subscribe(OnTaskCompleted);
+ _session.Events.Subscribe(OnTaskFailed);
+ _session.Events.Subscribe(OnTurnEnded);
}
public void Tick(float delta)
{
- // 预留:利息结算、工资消耗等
+ // 当前为回合驱动,不在 Tick 中结算
+ }
+
+ private void OnTaskCompleted(TaskCompletedEvent evt)
+ {
+ var task = evt.Task;
+ var economy = _session.State.Economy;
+ economy.Money += task.Reward.Money;
+ economy.Reputation += task.Reward.Reputation;
+
+ if (task.Reward.PaperIds.Count > 0)
+ {
+ foreach (var paperId in task.Reward.PaperIds)
+ {
+ if (_session.Content.Papers.TryGetValue(paperId, out var paper))
+ {
+ AddPaper(paper.Rank);
+ }
+ }
+ }
+ else if (task.Kind == TaskKind.AcademicExploration)
+ {
+ AddPaper(GetPaperRankByDifficulty(task.Difficulty));
+ }
+
+ foreach (var itemId in task.Reward.ItemIds)
+ {
+ AddItem(itemId, 1);
+ }
+ }
+
+ private void OnTaskFailed(TaskFailedEvent evt)
+ {
+ var task = evt.Task;
+ var penalty = task.Track == TaskTrack.Tenure ? 20 : 10;
+ if (task.Kind == TaskKind.GrantVertical) penalty += 10;
+ _session.State.Economy.Reputation -= penalty;
+ }
+
+ private void OnTurnEnded(TurnEndedEvent evt)
+ {
+ ApplySalaries();
+ ApplyInterest();
+ }
+
+ private void ApplySalaries()
+ {
+ var economy = _session.State.Economy;
+ foreach (var student in _session.State.Roster.Students)
+ {
+ economy.Money -= student.Type == StudentModel.StudentType.MasterCandidate
+ ? MasterSalary
+ : DoctorSalary;
+ }
+
+ foreach (var staff in _session.State.Roster.Staffs)
+ {
+ economy.Money -= staff.Type == StaffModel.StaffType.PostDoc
+ ? PostDocSalary
+ : JuniorFacultySalary;
+ }
+ }
+
+ private void ApplyInterest()
+ {
+ var economy = _session.State.Economy;
+ UpdateInterestRate();
+ if (economy.Money <= 0 || economy.InterestRate <= 0) return;
+
+ var interest = (int)(economy.Money * economy.InterestRate);
+ economy.Money += interest;
+ }
+
+ private void UpdateInterestRate()
+ {
+ var economy = _session.State.Economy;
+ var mentor = _session.State.Roster.Mentor;
+ if (mentor?.Core.Tags.DisciplineId == CoreIds.DisciplineEconomics)
+ {
+ economy.InterestRate = Math.Max(economy.InterestRate, 0.1f);
+ }
+ }
+
+ private void AddPaper(PaperRank rank)
+ {
+ var inventory = _session.State.Inventory;
+ if (!inventory.PaperCounts.ContainsKey(rank))
+ {
+ inventory.PaperCounts[rank] = 0;
+ }
+
+ inventory.PaperCounts[rank] += 1;
+ }
+
+ private void AddItem(string itemId, int count)
+ {
+ if (string.IsNullOrWhiteSpace(itemId)) return;
+ var inventory = _session.State.Inventory;
+ if (!inventory.ItemCounts.ContainsKey(itemId))
+ {
+ inventory.ItemCounts[itemId] = 0;
+ }
+
+ inventory.ItemCounts[itemId] += count;
+ }
+
+ private PaperRank GetPaperRankByDifficulty(TaskDifficulty difficulty)
+ {
+ return difficulty switch
+ {
+ TaskDifficulty.Water => PaperRank.C,
+ TaskDifficulty.Standard => PaperRank.B,
+ TaskDifficulty.Hardcore => PaperRank.A,
+ TaskDifficulty.BlackBox => PaperRank.S,
+ _ => PaperRank.C
+ };
}
}
@@ -101,7 +503,104 @@ public sealed class SynergySystem : IGameSystem
public void Tick(float delta)
{
- // 预留:根据 roster 统计羁绊层数
+ RecalculateSynergy();
+ }
+
+ private void RecalculateSynergy()
+ {
+ var state = _session.State.Synergy;
+ state.ArchetypeStacks.Clear();
+ state.RoleStacks.Clear();
+ state.ActiveSynergyIds.Clear();
+ ClearModifiers(state.ActiveModifiers);
+
+ CountUnitTags(state);
+ ApplySynergyDefinitions(state);
+ }
+
+ private void CountUnitTags(SynergyState synergy)
+ {
+ var roster = _session.State.Roster;
+ AddUnitTags(synergy, roster.Mentor?.Core);
+
+ foreach (var student in roster.Students)
+ {
+ AddUnitTags(synergy, student.Core);
+ }
+
+ foreach (var staff in roster.Staffs)
+ {
+ AddUnitTags(synergy, staff.Core);
+ }
+ }
+
+ private void AddUnitTags(SynergyState synergy, UnitModel unit)
+ {
+ if (unit == null) return;
+ foreach (var archetypeId in unit.Tags.ArchetypeIds)
+ {
+ AddStack(synergy.ArchetypeStacks, archetypeId);
+ }
+
+ foreach (var roleId in unit.Tags.RoleIds)
+ {
+ AddStack(synergy.RoleStacks, roleId);
+ }
+ }
+
+ private void AddStack(Dictionary stacks, string id)
+ {
+ if (string.IsNullOrWhiteSpace(id)) return;
+ if (!stacks.ContainsKey(id))
+ {
+ stacks[id] = 0;
+ }
+
+ stacks[id] += 1;
+ }
+
+ private void ApplySynergyDefinitions(SynergyState synergy)
+ {
+ foreach (var archetype in _session.Content.Archetypes.Values)
+ {
+ ApplySynergyTier(archetype.Header.Id, archetype.Tiers, synergy.ArchetypeStacks, synergy);
+ }
+
+ foreach (var role in _session.Content.Roles.Values)
+ {
+ ApplySynergyTier(role.Header.Id, role.Tiers, synergy.RoleStacks, synergy);
+ }
+ }
+
+ private void ApplySynergyTier(string id, List tiers, Dictionary stacks, SynergyState synergy)
+ {
+ if (string.IsNullOrWhiteSpace(id)) return;
+ stacks.TryGetValue(id, out var count);
+
+ foreach (var tier in tiers)
+ {
+ if (count < tier.RequiredCount) continue;
+ var synergyId = $"{id}@{tier.RequiredCount}";
+ synergy.ActiveSynergyIds.Add(synergyId);
+ MergeModifiers(synergy.ActiveModifiers, tier.Modifiers);
+ }
+ }
+
+ private void MergeModifiers(ModifierBundle target, ModifierBundle source)
+ {
+ if (target == null || source == null) return;
+ target.AttributeModifiers.AddRange(source.AttributeModifiers);
+ target.StatusModifiers.AddRange(source.StatusModifiers);
+ target.ResourceModifiers.AddRange(source.ResourceModifiers);
+ target.RuleIds.AddRange(source.RuleIds);
+ }
+
+ private void ClearModifiers(ModifierBundle bundle)
+ {
+ bundle.AttributeModifiers.Clear();
+ bundle.StatusModifiers.Clear();
+ bundle.ResourceModifiers.Clear();
+ bundle.RuleIds.Clear();
}
}
diff --git a/scripts/Core/StatResolver.cs b/scripts/Core/StatResolver.cs
new file mode 100644
index 0000000..d9d0089
--- /dev/null
+++ b/scripts/Core/StatResolver.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using Models;
+
+namespace Core;
+
+///
+/// 数值解析器(用于统一计算有效属性)
+/// 设计说明:
+/// 1) 只做“读+合成”,不直接修改 Model。
+/// 2) 合成来源包含:羁绊、学科 Buff、特质、装备。
+/// 注意事项:
+/// - 规则型效果(RuleIds)暂不在此处理,由系统层扩展。
+/// 未来扩展:
+/// - 可引入“上下文参数”(任务类型/场景)以处理条件加成。
+///
+public sealed class StatResolver
+{
+ private readonly GameSession _session;
+
+ public StatResolver(GameSession session)
+ {
+ _session = session;
+ }
+
+ public float GetAttribute(UnitModel unit, AttributeType type)
+ {
+ var value = GetBaseAttribute(unit, type);
+ ApplyBundle(_session.State.Synergy.ActiveModifiers, type, ref value);
+ ApplyDiscipline(unit.Tags.DisciplineId, type, ref value);
+ ApplyTraits(unit.Tags.TraitIds, type, ref value);
+ ApplyItems(unit.Equipment.EquippedItemIds, type, ref value);
+ return value;
+ }
+
+ private float GetBaseAttribute(UnitModel unit, AttributeType type)
+ {
+ return type switch
+ {
+ AttributeType.Academic => unit.Attributes.Academic.Value,
+ AttributeType.Engineering => unit.Attributes.Engineering.Value,
+ AttributeType.Writing => unit.Attributes.Writing.Value,
+ AttributeType.Financial => unit.Attributes.Financial.Value,
+ AttributeType.Social => unit.Attributes.Social.Value,
+ AttributeType.Activation => unit.Attributes.Activation.Value,
+ _ => 0f
+ };
+ }
+
+ private void ApplyDiscipline(string disciplineId, AttributeType type, ref float value)
+ {
+ if (string.IsNullOrWhiteSpace(disciplineId)) return;
+ if (!_session.Content.Disciplines.TryGetValue(disciplineId, out var discipline)) return;
+ ApplyBundle(discipline.Buff?.Modifiers, type, ref value);
+ }
+
+ private void ApplyTraits(List traitIds, AttributeType type, ref float value)
+ {
+ if (traitIds == null || traitIds.Count == 0) return;
+ foreach (var traitId in traitIds)
+ {
+ if (_session.Content.Traits.TryGetValue(traitId, out var trait))
+ {
+ ApplyBundle(trait.Modifiers, type, ref value);
+ }
+ }
+ }
+
+ private void ApplyItems(List itemIds, AttributeType type, ref float value)
+ {
+ if (itemIds == null || itemIds.Count == 0) return;
+ foreach (var itemId in itemIds)
+ {
+ if (_session.Content.Items.TryGetValue(itemId, out var item))
+ {
+ ApplyBundle(item.Effect?.Modifiers, type, ref value);
+ }
+ }
+ }
+
+ private void ApplyBundle(ModifierBundle bundle, AttributeType type, ref float value)
+ {
+ if (bundle == null) return;
+ foreach (var modifier in bundle.AttributeModifiers)
+ {
+ if (modifier.Type != type) continue;
+ value = (value + modifier.Add) * modifier.Multiplier;
+ }
+ }
+}
+
diff --git a/scripts/GameManager.cs b/scripts/GameManager.cs
index d6e7473..1f0bc57 100644
--- a/scripts/GameManager.cs
+++ b/scripts/GameManager.cs
@@ -127,28 +127,8 @@ public partial class GameManager : Node
///
private void PerformReview()
{
- // 1. Task progress check
- foreach (var task in ActiveTasks)
- {
- task.Runtime.RemainingTurns--;
- if (task.IsCompleted)
- {
- GD.Print($"Task {task.Name} Completed!");
- Economy.Reputation += task.Reward.Reputation;
- Economy.Money += task.Reward.Money;
- }
- else if (task.IsFailed)
- {
- GD.Print($"Task {task.Name} Failed!");
- Economy.Reputation -= 10; // Penalty
- }
- }
-
- // 2. Student status update (Salary, etc.)
- foreach (var student in Students)
- {
- // Deduct salary? Restore some stamina?
- }
+ Session.Systems.Task.ResolveEndOfTurn();
+ Session.Events.Publish(new TurnEndedEvent(CurrentTurn));
GD.Print("Review Complete. Waiting for Next Turn confirmation.");
}
diff --git a/scripts/Models/GameState.cs b/scripts/Models/GameState.cs
index e3e2531..bebf3a0 100644
--- a/scripts/Models/GameState.cs
+++ b/scripts/Models/GameState.cs
@@ -73,6 +73,7 @@ public sealed class SynergyState
public Dictionary ArchetypeStacks { get; } = new();
public Dictionary RoleStacks { get; } = new();
public List ActiveSynergyIds { get; } = new();
+ public ModifierBundle ActiveModifiers { get; } = new();
}
public sealed class RogueliteState
diff --git a/scripts/Models/Task.cs b/scripts/Models/Task.cs
index b779315..5671b3a 100644
--- a/scripts/Models/Task.cs
+++ b/scripts/Models/Task.cs
@@ -76,4 +76,5 @@ public sealed class TaskRewardSnapshot
public int Money { get; set; }
public int Reputation { get; set; }
public List PaperIds { get; } = new();
+ public List ItemIds { get; set; }
}