MVP models

This commit is contained in:
wjsjwr 2025-12-06 18:54:23 +08:00
parent 8e81f2a12b
commit 70c19e7972
19 changed files with 668 additions and 128 deletions

View File

@ -1,34 +0,0 @@
using Godot;
using System;
[Tool]
public partial class OkToPlayMobilePhone : BTCondition
{
[Export]
public StringName target;
public override Status _Tick(double delta)
{
Student student = Agent as Student; // Cast the agent to a Student object
if (student == null) return Status.Failure;
if (student.State == Student.CharacterState.Idle) {
return Status.Success;
}
if (student.State == Student.CharacterState.Sitting && student.TargetDirection == Student.Direction.Down) {
return Status.Success;
}
return Status.Failure;
}
public override string[] _GetConfigurationWarnings()
{
return Array.Empty<string>();
}
public override string _GenerateName()
{
return "OkToPlayMobilePhone";
}
}

View File

@ -1 +0,0 @@
uid://b3ukdvcoxly13

View File

@ -1,79 +0,0 @@
[gd_resource type="BehaviorTree" load_steps=14 format=3 uid="uid://cdsixeqsdfmc1"]
[ext_resource type="Script" uid="uid://b3ukdvcoxly13" path="res://ai/tasks/OkToPlayMobilePhone.cs" id="1_lfwnu"]
[sub_resource type="BlackboardPlan" id="BlackboardPlan_gwwr7"]
resource_local_to_scene = false
resource_name = ""
[sub_resource type="BTCondition" id="BTCondition_prnjq"]
resource_local_to_scene = false
resource_name = ""
target = &""
script = ExtResource("1_lfwnu")
target = &""
[sub_resource type="BBNode" id="BBNode_7pcle"]
resource_local_to_scene = false
resource_name = "AnimationPlayer"
saved_value = NodePath("AnimationPlayer")
[sub_resource type="BTPlayAnimation" id="BTPlayAnimation_hx8gl"]
resource_local_to_scene = false
resource_name = ""
await_completion = 1.2
animation_player = SubResource("BBNode_7pcle")
animation_name = &"phone_up"
[sub_resource type="BBNode" id="BBNode_6275u"]
resource_local_to_scene = false
resource_name = "AnimationPlayer"
saved_value = NodePath("AnimationPlayer")
[sub_resource type="BTPlayAnimation" id="BTPlayAnimation_wbk5l"]
resource_local_to_scene = false
resource_name = ""
await_completion = 1.0
animation_player = SubResource("BBNode_6275u")
animation_name = &"phone_loop"
[sub_resource type="BTRepeat" id="BTRepeat_1nqee"]
resource_local_to_scene = false
resource_name = ""
children = [SubResource("BTPlayAnimation_wbk5l")]
times = 3
[sub_resource type="BTProbability" id="BTProbability_x3mt2"]
resource_local_to_scene = false
resource_name = ""
children = [SubResource("BTRepeat_1nqee")]
run_chance = 0.75
[sub_resource type="BTRepeatUntilFailure" id="BTRepeatUntilFailure_uatr3"]
resource_local_to_scene = false
resource_name = ""
children = [SubResource("BTProbability_x3mt2")]
[sub_resource type="BBNode" id="BBNode_2433i"]
resource_local_to_scene = false
resource_name = "AnimationPlayer"
saved_value = NodePath("AnimationPlayer")
[sub_resource type="BTPlayAnimation" id="BTPlayAnimation_g4biw"]
resource_local_to_scene = false
resource_name = ""
await_completion = 0.5
animation_player = SubResource("BBNode_2433i")
animation_name = &"phone_down"
[sub_resource type="BTSequence" id="BTSequence_0drub"]
resource_local_to_scene = false
resource_name = ""
children = [SubResource("BTCondition_prnjq"), SubResource("BTPlayAnimation_hx8gl"), SubResource("BTRepeatUntilFailure_uatr3"), SubResource("BTPlayAnimation_g4biw")]
[resource]
resource_local_to_scene = false
resource_name = ""
description = "WatchMobilePhone"
blackboard_plan = SubResource("BlackboardPlan_gwwr7")
root_task = SubResource("BTSequence_0drub")

View File

@ -933,15 +933,3 @@ libraries = {
[node name="StudentName" type="Node" parent="."]
script = ExtResource("8_kvqca")
[node name="BTPlayer" type="BTPlayer" parent="."]
_import_path = NodePath("")
unique_name_in_owner = false
process_mode = 0
process_priority = 0
process_physics_priority = 0
process_thread_group = 0
physics_interpolation_mode = 0
auto_translate_mode = 0
editor_description = ""
script = null

View File

@ -1,5 +1,7 @@
using Godot;
using System;
using System.Collections.Generic;
using Models;
public partial class GameManager : Node
{
@ -9,17 +11,154 @@ public partial class GameManager : Node
public static bool IsTutorial { get; private set; }
public static string NextScene { get; set; } = null;
public static readonly Resource Arrow2x = ResourceLoader.Load("res://temp_res/kenney_ui-pack-space-expansion/PNG/Extra/Double/cursor_f.png");
// Called when the node enters the scene tree for the first time.
// --- Core Loop Definitions ---
public enum GamePhase
{
Planning, // 筹备阶段:时间暂停,分配任务,购买设施
Execution, // 执行阶段:时间流动,学生工作
Review, // 结算阶段:回合结束,发工资,结算成果
}
[Export]
public int MaxTurns = 30;
// --- Global State ---
public static GamePhase CurrentPhase { get; private set; } = GamePhase.Planning;
public static int CurrentTurn { get; private set; } = 1;
// --- Domain Model ---
public MentorModel Mentor { get; private set; } = new MentorModel();
public List<StudentModel> Students { get; private set; } = new List<StudentModel>();
public List<Task> ActiveTasks { get; private set; } = new List<Task>();
// --- Signals ---
[Signal] public delegate void PhaseChangedEventHandler(int phase); // int cast of GamePhase
[Signal] public delegate void TurnChangedEventHandler(int turn);
// Singleton instance access (if needed, though Godot uses node paths)
public static GameManager Instance { get; private set; }
public override void _EnterTree()
{
Instance = this;
}
public override void _Ready()
{
Input.SetCustomMouseCursor(Arrow2x);
// MVP Initialization
InitializeGame();
}
private void InitializeGame()
{
CurrentTurn = 1;
CurrentPhase = GamePhase.Planning;
// MVP: Add a test student
var s1 = new StudentModel("张三");
Students.Add(s1);
// MVP: Add a test task
var t1 = new Task("深度学习导论", TaskType.Paper, 1000f, 3);
ActiveTasks.Add(t1);
GD.Print("Game Initialized. Phase: Planning, Turn: 1");
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (CurrentPhase == GamePhase.Execution)
{
// Update game logic (timers, etc.)
// In a real implementation, this might manage the global timer for the day/week
}
}
/// <summary>
/// 结束筹备阶段,开始执行
/// </summary>
public void StartExecution()
{
if (CurrentPhase != GamePhase.Planning) return;
CurrentPhase = GamePhase.Execution;
EmitSignal(SignalName.PhaseChanged, (int)CurrentPhase);
GD.Print("Phase Changed: Execution");
// Notify all agents to start working (this would be handled by a system listening to the signal)
}
/// <summary>
/// 结束执行阶段(通常由时间耗尽或玩家手动触发),进入结算
/// </summary>
public void EndExecution()
{
if (CurrentPhase != GamePhase.Execution) return;
CurrentPhase = GamePhase.Review;
EmitSignal(SignalName.PhaseChanged, (int)CurrentPhase);
GD.Print("Phase Changed: Review");
PerformReview();
}
/// <summary>
/// 执行结算逻辑,并准备下一回合
/// </summary>
private void PerformReview()
{
// 1. Task progress check
foreach (var task in ActiveTasks)
{
task.Deadline--;
if (task.IsCompleted)
{
GD.Print($"Task {task.Name} Completed!");
Mentor.Reputation += task.RewardReputation;
Mentor.Money += task.RewardMoney;
}
else if (task.IsFailed)
{
GD.Print($"Task {task.Name} Failed!");
Mentor.Reputation -= 10; // Penalty
}
}
// 2. Student status update (Salary, etc.)
foreach (var student in Students)
{
// Deduct salary? Restore some stamina?
}
GD.Print("Review Complete. Waiting for Next Turn confirmation.");
}
/// <summary>
/// 开始新的回合
/// </summary>
public void StartNextTurn()
{
if (CurrentPhase != GamePhase.Review) return;
CurrentTurn++;
if (CurrentTurn > MaxTurns)
{
GD.Print("Game Over!");
// Handle Game Over
return;
}
CurrentPhase = GamePhase.Planning;
EmitSignal(SignalName.TurnChanged, CurrentTurn);
EmitSignal(SignalName.PhaseChanged, (int)CurrentPhase);
// Refresh resources/AP
Mentor.Energy.Current = Mentor.Energy.UpperThreshold;
GD.Print($"Turn {CurrentTurn} Started. Phase: Planning");
}
}

View File

@ -0,0 +1,40 @@
namespace Models;
/// <summary>
/// 导师数据模型 (MentorModel)
/// 玩家的数值状态。
/// </summary>
public class MentorModel
{
public enum MentorModeType
{
Worker,
Manager
}
/// <summary>
/// 精力值 (Energy)
/// 用于释放技能画饼、PUA、甚至亲自写代码
/// 每回合恢复。
/// </summary>
public StatusValue Energy { get; set; }
/// <summary>
/// 经费 (Money/Funds)
/// 单位:元。用于发工资、买设备。
/// </summary>
public int Money { get; set; } = 50000;
/// <summary>
/// 学术声望 (Reputation)
/// 影响招生质量、项目申请成功率。
/// </summary>
public int Reputation { get; set; } = 0;
/// <summary>
/// 算力/数据资源 (ResearchPoints)
/// 用于攻克高难度 AI 课题。
/// </summary>
public int ResearchPoints { get; set; } = 0;
public MentorModeType Mode { get; set; } = MentorModeType.Worker;
}

View File

@ -0,0 +1 @@
uid://dyehdtnucytg2

View File

@ -0,0 +1,122 @@
using System;
namespace Models;
/// <summary>
/// 属性值类型
/// 封装了整型数值,提供最大值限制(100)和显示转换。
/// </summary>
public class PropertyValue
{
public const int Min = 0;
public const int Max = 100;
private float _value;
/// <summary>
/// 获取或设置当前值。
/// 设置时会自动限制在 [Min, Max] 范围内。
/// </summary>
public float Value
{
get => _value;
set
{
_value = value;
if (_value > Max) _value = Max;
if (_value < Min) _value = Min;
}
}
public PropertyValue(int value)
{
Value = value;
}
public PropertyValue(float value)
{
Value = value;
}
public PropertyValue() : this(0) { }
/// <summary>
/// 获取显示值 (Value / 100.0f)
/// </summary>
public int Display() => (int)_value;
public override string ToString()
{
return Display().ToString();
}
// --- 隐式转换 (实现“重载等号”效果) ---
// 允许 int 直接赋值给 PropertyValue (创建新实例)
public static implicit operator PropertyValue(int value)
{
return new PropertyValue(value);
}
// 允许 int 直接赋值给 PropertyValue (创建新实例)
public static implicit operator PropertyValue(float value)
{
return new PropertyValue(value);
}
// 允许 PropertyValue 像 int 一样使用
public static implicit operator int(PropertyValue p)
{
return (int)p._value;
}
// 允许 PropertyValue 像 float 一样使用
public static implicit operator float(PropertyValue p)
{
return p._value;
}
// --- 运算符重载 (支持 int 计算) ---
public static PropertyValue operator +(PropertyValue a, int b) => new(a._value + b);
public static PropertyValue operator +(int a, PropertyValue b) => new(a + b._value);
public static PropertyValue operator -(PropertyValue a, int b) => new(a._value - b);
public static PropertyValue operator -(int a, PropertyValue b) => new(a - b._value);
public static PropertyValue operator *(PropertyValue a, int b) => new(a._value * b);
public static PropertyValue operator *(int a, PropertyValue b) => new(a * b._value);
public static PropertyValue operator /(PropertyValue a, int b)
{
return b == 0 ? throw new DivideByZeroException() : new PropertyValue(a._value / b);
}
public static PropertyValue operator /(int a, PropertyValue b)
{
return b._value == 0 ? throw new DivideByZeroException() : new PropertyValue(a / b._value);
}
// --- 运算符重载 (支持 float 计算) ---
public static PropertyValue operator +(PropertyValue a, float b) => new(a._value + b);
public static PropertyValue operator +(float a, PropertyValue b) => new(a + b._value);
public static PropertyValue operator -(PropertyValue a, float b) => new(a._value - b);
public static PropertyValue operator -(float a, PropertyValue b) => new(a - b._value);
public static PropertyValue operator *(PropertyValue a, float b) => new(a._value * b);
public static PropertyValue operator *(float a, PropertyValue b) => new(a * b._value);
public static PropertyValue operator /(PropertyValue a, float b) => new(a._value / b);
public static PropertyValue operator /(float a, PropertyValue b) => new(a / b._value);
// --- 运算符重载 (PropertyValue 之间) ---
public static PropertyValue operator +(PropertyValue a, PropertyValue b) => new(a._value + b._value);
public static PropertyValue operator -(PropertyValue a, PropertyValue b) => new(a._value - b._value);
public static PropertyValue operator *(PropertyValue a, PropertyValue b) => new(a._value * b._value);
public static PropertyValue operator /(PropertyValue a, PropertyValue b)
{
return b._value == 0 ? throw new DivideByZeroException() : new PropertyValue(a._value / b._value);
}
}

View File

@ -0,0 +1 @@
uid://l58i2kmspq7c

View File

@ -0,0 +1,101 @@
using System;
namespace Models;
/// <summary>
/// 状态值 (StatusValue)
/// 维护当前状态值以及上限和下限阈值。
/// </summary>
public class StatusValue
{
private PropertyValue _current;
private PropertyValue _upperThreshold;
private PropertyValue _lowerThreshold;
/// <summary>
/// 当 Current >= UpperThreshold 时触发此事件
/// </summary>
public event Action OnUpperThresholdReached;
/// <summary>
/// 当 Current <= LowerThreshold 时触发此事件
/// </summary>
public event Action OnLowerThresholdReached;
/// <summary>
/// 当前状态值
/// 修改时会自动检测阈值。
/// </summary>
public PropertyValue Current
{
get => _current;
set
{
_current = value;
CheckThresholds();
}
}
/// <summary>
/// 上限阈值
/// </summary>
public PropertyValue UpperThreshold
{
get => _upperThreshold;
set
{
_upperThreshold = value;
CheckThresholds();
}
}
/// <summary>
/// 下限阈值
/// </summary>
public PropertyValue LowerThreshold
{
get => _lowerThreshold;
set
{
_lowerThreshold = value;
CheckThresholds();
}
}
public StatusValue(int current = 0, int upper = 100, int lower = 0)
{
_current = new PropertyValue(current);
_upperThreshold = new PropertyValue(upper);
_lowerThreshold = new PropertyValue(lower);
}
public StatusValue(PropertyValue current, PropertyValue upper, PropertyValue lower)
{
_current = current ?? new PropertyValue(0);
_upperThreshold = upper ?? new PropertyValue(100);
_lowerThreshold = lower ?? new PropertyValue(0);
}
private void CheckThresholds()
{
if ((int)_current >= (int)_upperThreshold)
{
OnUpperThresholdReached?.Invoke();
}
if ((int)_current <= (int)_lowerThreshold)
{
OnLowerThresholdReached?.Invoke();
}
}
public void Add(int value)
{
Current += value; // 触发 setter -> CheckThresholds
}
public void Subtract(int value)
{
Current -= value; // 触发 setter -> CheckThresholds
}
}

View File

@ -0,0 +1 @@
uid://no453n0ubri1

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
namespace Models;
/// <summary>
/// 学生数据模型 (StudentModel)
/// 包含学生的数值、状态和特质。
/// 与 Godot 的 Node (View) 分离,便于序列化和逻辑处理。
/// </summary>
public class StudentModel: UnitModel
{
public enum StudentType
{
MasterCandidate,
DoctorCandidate
}
// --- 静态属性 (Property) ---
/// <summary>
/// 学生类型
/// </summary>
public StudentType Type { get; private set; }
// --- 动态状态 (Status) ---
/// <summary>
/// 体力 (Stamina)
/// 范围 0-100。工作消耗体力休息恢复。体力过低效率下降。
/// </summary>
public StatusValue Stamina { get; set; }
/// <summary>
/// 忠诚度 (Loyalty)
/// 范围 0-100。过低可能跳槽或举报。
/// </summary>
public StatusValue Loyalty { get; set; }
/// <summary>
/// 年级
/// </summary>
public StatusValue Grade { get; set; }
/// <summary>
/// 记录对每个 Task 的贡献量,用于署名分配
/// </summary>
public Dictionary<Guid, PropertyValue> Contributions { get; set; }
/// <summary>
/// 特质列表 (如 "DDL战士", "卷王")
/// </summary>
public List<string> Traits { get; set; } = [];
public StudentModel(string name) : base(name)
{
var random = new Random();
Stamina = new StatusValue(random.Next(100),100, 0);
Loyalty = new StatusValue(80, 100, 0);
Type = random.Next(2) == 0 ? StudentType.MasterCandidate : StudentType.DoctorCandidate;
Grade = new StatusValue(0, Type == StudentType.DoctorCandidate ? 6 : 3);
}
}

View File

@ -0,0 +1 @@
uid://eoh7yvyhct5d

97
scripts/Models/Task.cs Normal file
View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
namespace Models;
/// <summary>
/// 任务类型枚举
/// </summary>
public enum TaskType
{
Paper, // 论文:重学术(Intelligence) + 表达(Expression)
Project, // 项目:重工程(Strength) + 学术(Intelligence)
Admin // 杂务:消耗体力,低收益,但必须做
}
/// <summary>
/// 任务 (Task)
/// 核心游戏对象之一,相当于地图上的“怪物”。
/// 学生需要“攻击”任务(消耗工作量)来完成它。
/// </summary>
public class Task
{
public Guid Id { get; private set; } = Guid.NewGuid();
/// <summary>
/// 任务名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 任务类型
/// </summary>
public TaskType Type { get; set; }
/// <summary>
/// 总工作量 (HP)
/// </summary>
public float TotalWorkload { get; set; }
/// <summary>
/// 当前进度
/// </summary>
public float CurrentProgress { get; set; }
/// <summary>
/// 难度系数 (Defense)
/// 影响学生攻克该任务的效率。若学生能力低于难度,效率会大幅下降。
/// </summary>
public float Difficulty { get; set; }
/// <summary>
/// 截止日期 (剩余回合数)
/// 归零时如果未完成,触发失败惩罚。
/// </summary>
public int Deadline { get; set; }
/// <summary>
/// 奖励:经费
/// </summary>
public int RewardMoney { get; set; }
/// <summary>
/// 奖励:声望
/// </summary>
public int RewardReputation { get; set; }
/// <summary>
/// 任务是否已完成
/// </summary>
public bool IsCompleted => CurrentProgress >= TotalWorkload;
/// <summary>
/// 任务是否已失败(超时未完成)
/// </summary>
public bool IsFailed => Deadline <= 0 && !IsCompleted;
public Task(string name, TaskType type, float workload, int deadline)
{
Name = name;
Type = type;
TotalWorkload = workload;
Deadline = deadline;
CurrentProgress = 0;
Difficulty = 1.0f;
}
/// <summary>
/// 增加进度
/// </summary>
/// <param name="amount">工作量数值</param>
public void AddProgress(float amount)
{
CurrentProgress += amount;
if (CurrentProgress > TotalWorkload) CurrentProgress = TotalWorkload;
}
}

View File

@ -0,0 +1 @@
uid://052kca2012sn

View File

@ -0,0 +1,78 @@
using Godot;
using System;
namespace Models;
/// <summary>
/// 所有角色的基类,无论是导师、学生还是博后。
/// </summary>
public class UnitModel
{
public Guid Id { get; private set; } = Guid.NewGuid();
public string Name { get; set; }
// 基础属性
/// <summary>
/// 学术能力,影响科研进度
/// </summary>
public PropertyValue Academic {get; set;}
/// <summary>
/// 工程能力,影响实验进度
/// </summary>
public PropertyValue Engineering {get; set;}
/// <summary>
/// 写作能力,影响论文发表
/// </summary>
public PropertyValue Writing {get; set;}
/// <summary>
/// 财务能力,影响资金相关任务
/// </summary>
public PropertyValue Financial {get; set;}
/// <summary>
/// 社交能力
/// </summary>
public PropertyValue Social {get; set;}
/// <summary>
/// 行动力,影响各种行为的效率和能动性
/// </summary>
public PropertyValue Activation {get; set;}
// 状态属性
/// <summary>
/// 压力值
/// </summary>
public StatusValue Stress {get; set;}
/// <summary>
/// 精神值 (Sanity) / 抗压能力
/// 范围 0-100。过低会导致崩溃(Breakdown)。
/// </summary>
public StatusValue Sanity { get; set; }
/// <summary>
/// 情绪值,影响工作效率
/// </summary>
public PropertyValue Mood {get; set;}
/// <summary>
/// 移动速度
/// </summary>
public float MoveSpeed {get; set;}
// 局内属性
public Vector2I TargetPosition {get; set;}
public Vector2I CurrentPosition {get; set;}
public Guid TargetTaskId {get; set;}
protected UnitModel(string name)
{
Name = name;
var random = new Random();
Academic = random.Next(PropertyValue.Min, PropertyValue.Max);
Engineering = random.Next(PropertyValue.Min, PropertyValue.Max);
Writing = random.Next(PropertyValue.Min, PropertyValue.Max);
Financial = random.Next(PropertyValue.Min, PropertyValue.Max);
Social = random.Next(PropertyValue.Min, PropertyValue.Max);
Activation = random.Next(PropertyValue.Min, PropertyValue.Max);
}
}

View File

@ -0,0 +1 @@
uid://hremnrofal6l

View File

@ -2,6 +2,7 @@ using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using Models;
// ReSharper disable CheckNamespace
public partial class Student : CharacterBody2D
@ -9,6 +10,16 @@ public partial class Student : CharacterBody2D
public float Speed { get; set; } = 8.0f;
public const float JumpVelocity = -400.0f;
// --- MVP: Model Binding ---
public StudentModel Model { get; private set; }
public void BindData(StudentModel model)
{
Model = model;
GD.Print($"Student bound to model: {Model.Name}");
}
// --------------------------
public int NextType = -1;
private Queue<Vector2I> PathToGo = new();
@ -140,7 +151,15 @@ public partial class Student : CharacterBody2D
animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
animationPlayer.Play("idle_front");
var name_test = GetNode<StudentName>("StudentName");
GD.Print("生成的名字是: " + name_test.GenerateName());
// MVP: Initialize Model if null
if (Model == null)
{
Model = new StudentModel(name_test.GenerateName());
GD.Print($"[Auto-Init] Student: {Model.Name}");
} else {
GD.Print("生成的名字是: " + name_test.GenerateName()); // Keep original log for reference
}
}
public void MoveFollowPath(List<Vector2I> path)

View File

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<Content Include="docs\design.md" />
<Content Include="docs\detail_design.md" />
<Content Include="docs\mvp_design.md" />
<Content Include="README.md" />
</ItemGroup>