From 9c1593e7171e21651f79345177b376247724d15c Mon Sep 17 00:00:00 2001 From: wjsjwr Date: Thu, 1 Jan 2026 00:07:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=B2=E6=8A=8A=20TaskSystem/SynergySystem/E?= =?UTF-8?q?conomySystem=20=E6=8C=89=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E7=9A=84=E2=80=9C=E6=A0=B8=E5=BF=83=E8=A7=84=E5=88=99=E2=80=9D?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=E6=88=90=E5=8F=AF=E8=BF=90=E8=A1=8C=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=B9=B6=E6=8A=8A=E5=9B=9E=E5=90=88=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E4=BB=8E=20GameManager=20=E4=B8=8B=E6=B2=89=E5=88=B0?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E4=B8=8E=E4=BA=8B=E4=BB=B6=E6=B5=81=E4=B8=8A?= =?UTF-8?q?=E3=80=82=20=20=20-=20=E4=BB=BB=E5=8A=A1=E6=8E=A8=E8=BF=9B?= =?UTF-8?q?=E4=B8=8E=E7=BB=93=E7=AE=97=EF=BC=9A=E6=89=A7=E8=A1=8C=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E6=8C=89=E4=BB=BB=E5=8A=A1=E7=B1=BB=E5=9E=8B=E7=94=A8?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E6=9D=83=E9=87=8D=E8=AE=A1=E7=AE=97=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E3=80=81=E8=80=83=E8=99=91=E8=A7=92=E8=89=B2/?= =?UTF-8?q?=E5=AD=A6=E7=A7=91=E5=8C=B9=E9=85=8D=E3=80=81=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E8=A1=B0=E5=87=8F=E4=B8=8E=E9=9A=BE=E5=BA=A6=E7=BC=A9=E6=94=BE?= =?UTF-8?q?=EF=BC=8C=E5=9B=9E=E5=90=88=E7=BB=93=E6=9D=9F=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=89=A3=20Deadline=E3=80=81=E5=88=A4=E5=AE=9A=E5=AE=8C?= =?UTF-8?q?=E6=88=90/=E5=A4=B1=E8=B4=A5=E5=B9=B6=E6=B4=BE=E5=8F=91?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=EF=BC=8C=E5=90=8C=E6=97=B6=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E7=BD=B2=E5=90=8D=E8=B4=A1=E7=8C=AE=E5=BA=A6=20scripts/Core/Ga?= =?UTF-8?q?meSystems.cs=20=20=20-=20=E7=BB=8F=E6=B5=8E=E7=BB=93=E7=AE=97?= =?UTF-8?q?=EF=BC=9A=E6=8E=A5=E6=94=B6=E4=BB=BB=E5=8A=A1=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=8F=91=E5=A5=96=E6=83=A9=EF=BC=8C=E5=AD=A6=E6=9C=AF=E6=8E=A2?= =?UTF-8?q?=E7=B4=A2=E6=A0=B9=E6=8D=AE=E9=9A=BE=E5=BA=A6=E7=94=9F=E6=88=90?= =?UTF-8?q?=E8=AE=BA=E6=96=87=E5=8D=A1=EF=BC=9B=E5=9B=9E=E5=90=88=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E7=BB=9F=E4=B8=80=E6=89=A3=E5=B7=A5=E8=B5=84=E5=B9=B6?= =?UTF-8?q?=E7=BB=93=E6=81=AF=EF=BC=88=E7=BB=8F=E6=B5=8E=E5=AD=A6=E5=AD=A6?= =?UTF-8?q?=E7=A7=91=E8=A7=A6=E5=8F=91=E5=9F=BA=E7=A1=80=E5=88=A9=E7=8E=87?= =?UTF-8?q?=EF=BC=89scripts/Core/GameSystems.cs=20=20=20-=20=E7=BE=81?= =?UTF-8?q?=E7=BB=8A=E5=B1=82=E6=95=B0=E4=B8=8E=E6=95=88=E6=9E=9C=EF=BC=9A?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E4=BA=BA=E7=BE=A4=E7=94=BB=E5=83=8F/?= =?UTF-8?q?=E8=81=8C=E8=83=BD=E5=88=86=E5=B7=A5=E5=A0=86=E5=8F=A0=EF=BC=8C?= =?UTF-8?q?=E6=BF=80=E6=B4=BB=E5=B1=82=E7=BA=A7=E5=B9=B6=E8=81=9A=E5=90=88?= =?UTF-8?q?=E4=B8=BA=E5=85=A8=E5=B1=80=20ModifierBundle=20=E7=BB=99?= =?UTF-8?q?=E6=95=B0=E5=80=BC=E8=A7=A3=E6=9E=90=E5=99=A8=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=20scripts/Core/GameSystems.cs,=20scripts/Models/GameState.cs?= =?UTF-8?q?=20=20=20-=20=E4=BA=8B=E4=BB=B6=E4=B8=8E=E6=95=B0=E5=80=BC?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E6=94=AF=E6=92=91=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=A2=86=E5=9F=9F=E4=BA=8B=E4=BB=B6=E4=B8=8E=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=90=88=E6=88=90=E5=99=A8=EF=BC=8C=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E7=BE=81=E7=BB=8A/=E5=AD=A6=E7=A7=91/=E7=89=B9?= =?UTF-8?q?=E8=B4=A8/=E8=A3=85=E5=A4=87=E7=9A=84=E5=8F=A0=E5=8A=A0=20scrip?= =?UTF-8?q?ts/Core/DomainEvents.cs,=20scripts/Core/StatResolver.cs=20=20?= =?UTF-8?q?=20-=20=E5=9B=9E=E5=90=88=E7=BB=93=E7=AE=97=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E8=B0=83=E6=95=B4=EF=BC=9AReview=20=E9=98=B6=E6=AE=B5=E6=94=B9?= =?UTF-8?q?=E7=94=B1=E7=B3=BB=E7=BB=9F=E5=A4=84=E7=90=86=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=9B=9E=E5=90=88=E7=BB=93=E6=9D=9F=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=20scripts/GameManager.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/Core/DomainEvents.cs | 44 +++ scripts/Core/GameSystems.cs | 507 ++++++++++++++++++++++++++++++++++- scripts/Core/StatResolver.cs | 90 +++++++ scripts/GameManager.cs | 24 +- scripts/Models/GameState.cs | 1 + scripts/Models/Task.cs | 1 + 6 files changed, 641 insertions(+), 26 deletions(-) create mode 100644 scripts/Core/DomainEvents.cs create mode 100644 scripts/Core/StatResolver.cs 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; } }