supervisor-simulator/scripts/Student.cs
2026-01-18 20:05:23 +08:00

324 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using Models;
// ReSharper disable CheckNamespace
/// <summary>
/// 学生角色控制器
/// </summary>
public partial class Student : CharacterBody2D {
/// <summary>
/// 角色状态枚举
/// </summary>
public enum CharacterState {
Idle,
Walking,
Sitting,
SittingDown,
StandingUp
}
/// <summary>
/// 方向枚举
/// </summary>
public enum Direction {
Up,
Down,
Left,
Right
}
/// <summary>
/// 跳跃速度
/// </summary>
public const float JumpVelocity = -400.0f;
private readonly Queue<Vector2I> _pathToGo = new();
private AnimationPlayer _animationPlayer;
private double _duration;
private Vector2I _lastWalkablePosition;
private Vector2I _targetSpecialPosition;
/// <summary>
/// 关联的格子ID
/// </summary>
public Guid CubeId;
// --------------------------
/// <summary>
/// 下一个状态类型(未使用)
/// </summary>
public int NextType = -1;
/// <summary>
/// 移动速度
/// </summary>
public float Speed { get; set; } = 8.0f;
/// <summary>
/// 是否使用16x16精灵
/// </summary>
[Export]
public bool Use16X16Sprites { get; set; }
// --- MVP: Model Binding ---
/// <summary>
/// 学生数据模型
/// </summary>
public StudentModel Model { get; private set; }
/// <summary>
/// 当前状态
/// </summary>
public CharacterState State { get; set; } = CharacterState.Idle;
/// <summary>
/// 目标方向
/// </summary>
public Direction TargetDirection { get; set; } = Direction.Up;
/// <summary>
/// 状态队列
/// </summary>
public Queue<CharacterState> StateQueue { get; set; } = new();
/// <summary>
/// 绑定数据模型
/// </summary>
/// <param name="model">学生模型</param>
public void BindData(StudentModel model) {
Model = model;
GD.Print($"Student bound to model: {Model.Name}");
}
private void SitDown(double delta) {
_duration += delta;
if (!(_duration > 0.3)) return;
if (TargetDirection == Direction.Up) { }
_animationPlayer.Play("idle_front");
State = StateQueue.Dequeue();
GlobalPosition = _targetSpecialPosition;
}
private void GettingUp(double delta) {
_duration += delta;
if (!(_duration > 0.3)) return;
State = StateQueue.Dequeue();
GlobalPosition = _lastWalkablePosition;
}
/// <summary>
/// 物理处理
/// </summary>
/// <param name="delta">时间间隔</param>
public override void _PhysicsProcess(double delta) {
if (StateQueue.Count > 0) {
// GD.Print($"{State} -> {StateQueue.Peek()}");
if (StateQueue.Peek() == CharacterState.Sitting && State == CharacterState.SittingDown) {
SitDown(delta);
return; // do not handle potentially conflicting command
}
if (StateQueue.Peek() == CharacterState.StandingUp && State == CharacterState.Sitting) {
_duration = 0;
State = StateQueue.Dequeue();
return;
}
if (StateQueue.Peek() == CharacterState.Walking && State == CharacterState.StandingUp) {
GettingUp(delta);
return;
}
}
// GD.Print($"{State} -> Empty");
if (_pathToGo.Count == 0) {
if (State != CharacterState.Walking) return;
if (StateQueue.Count > 0) {
if (StateQueue.Peek() == CharacterState.SittingDown) _duration = 0;
State = StateQueue.Dequeue();
}
if (State == CharacterState.Idle) _animationPlayer.Play("idle_front");
return; // No path to follow.
}
if (State != CharacterState.Walking) return;
Vector2 velocity = new();
var nextPoint = _pathToGo.Peek();
if ((int)GlobalPosition.X == nextPoint.X && (int)GlobalPosition.Y == nextPoint.Y) {
_pathToGo.Dequeue();
return;
}
if ((int)GlobalPosition.X == nextPoint.X) {
// Move Y
// velocity.Y = Math.Max(Speed, Math.Abs(nextPoint.Y - GlobalPosition.Y));
velocity.Y = Speed;
if (GlobalPosition.Y > nextPoint.Y) {
_animationPlayer.Play("walk_up");
velocity.Y = -velocity.Y;
}
else {
_animationPlayer.Play("walk_down");
}
}
else if ((int)GlobalPosition.Y == nextPoint.Y) {
// move X
// velocity.X = Math.Max(Speed, Math.Abs(nextPoint.X - GlobalPosition.X));
velocity.X = Speed;
if (GlobalPosition.X > nextPoint.X) {
_animationPlayer.Play("walk_left");
velocity.X = -velocity.X;
}
else {
_animationPlayer.Play("walk_right");
}
}
GlobalPosition = GlobalPosition with {
X = GlobalPosition.X + velocity.X,
Y = GlobalPosition.Y + velocity.Y
};
// GD.Print($"{GlobalPosition} -> {nextPoint}");
// Velocity = velocity;
// MoveAndSlide();
}
/// <summary>
/// 准备就绪时调用
/// </summary>
public override void _Ready() {
base._Ready();
// var bt = GetNode<BTPlayer>("BTPlayer");
// var bb = bt.Blackboard;
// bb.BindVarToProperty("Stayed", this, "Speed");
// GD.Print(bb.GetVar("Stayed"));
// GD.Print($"Speed: {Speed}");
_animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
_animationPlayer.Play("idle_front");
var nameTest = GetNode<StudentName>("StudentName");
// MVP: Initialize Model if null
if (Model == null) {
Model = new StudentModel(nameTest.GenerateName());
GD.Print($"[Auto-Init] Student: {Model.Name}");
}
else {
GD.Print("生成的名字是: " + nameTest.GenerateName()); // Keep original log for reference
}
}
/// <summary>
/// 移动跟随路径
/// </summary>
/// <param name="path">路径点列表</param>
public void MoveFollowPath(List<Vector2I> path) {
foreach (var p in path) _pathToGo.Enqueue(new Vector2I(p.X * 48 + 24, p.Y * 48 + 24));
}
/// <summary>
/// 随机前往某处
/// </summary>
public void GoTo() {
if (State == CharacterState.SittingDown || State == CharacterState.StandingUp ||
State == CharacterState.Walking) return;
var lab = GetParentOrNull<Lab>();
if (lab == null) return;
var vec = Vector2I.Zero;
while ((lab.GetMapNodeTypeOfPosition(vec) & Lab.MapNodeType.Walkable) == 0) {
vec.X = GD.RandRange(0, Lab.MapWidth);
vec.Y = GD.RandRange(0, Lab.MapHeight);
}
var pos = GlobalPosition;
if (State == CharacterState.Sitting) {
StateQueue.Enqueue(CharacterState.StandingUp);
StateQueue.Enqueue(CharacterState.Walking);
pos = _lastWalkablePosition;
}
else if (State == CharacterState.Idle) {
State = CharacterState.Walking;
}
StateQueue.Enqueue(CharacterState.Idle);
MoveFollowPath(lab.GetShortestPath(lab.Point2Coord(pos), vec));
RandomChangeBody();
}
/// <summary>
/// 前往座位
/// </summary>
public void GoToSeat() {
if (State == CharacterState.SittingDown || State == CharacterState.StandingUp ||
State == CharacterState.Walking) return;
var lab = GetParentOrNull<Lab>();
if (lab == null) return;
var i = GD.RandRange(0, 1);
var target = lab.GetFurSpecialPosition(CubeId, i);
if (i == 1) {
// Seat up
TargetDirection = Direction.Up;
_targetSpecialPosition = new Vector2I(target.X * 48 + 24, target.Y * 48 + 12);
}
else {
TargetDirection = Direction.Down;
_targetSpecialPosition = new Vector2I(target.X * 48 + 24, target.Y * 48 + 18);
}
var pos = GlobalPosition;
if (State == CharacterState.Sitting) {
StateQueue.Enqueue(CharacterState.StandingUp);
StateQueue.Enqueue(CharacterState.Walking);
pos = _lastWalkablePosition;
}
else if (State == CharacterState.Idle) {
State = CharacterState.Walking;
}
StateQueue.Enqueue(CharacterState.SittingDown);
StateQueue.Enqueue(CharacterState.Sitting);
var path = lab.GetShortestPath(lab.Point2Coord(pos), target);
_lastWalkablePosition = new Vector2I(path.Last().X * 48 + 24, path.Last().Y * 48 + 24);
// if (lastWalkablePosition.X < targetSpecialPosition.X) {
// TargetDirection = Direction.Right;
// } else if (lastWalkablePosition.X > targetSpecialPosition.X) {
// TargetDirection = Direction.Left;
// } else {
// if (lastWalkablePosition.Y < targetSpecialPosition.Y) {
// TargetDirection = Direction.Up;
// } else if (lastWalkablePosition.Y > targetSpecialPosition.Y) {
// TargetDirection = Direction.Down;
// }
// }
MoveFollowPath(path);
RandomChangeBody();
}
/// <summary>
/// 随机更换外观
/// </summary>
private void RandomChangeBody() {
GetNode<Sprite2D>("parts/body").Texture =
ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Body, Use16X16Sprites));
GetNode<Sprite2D>("parts/hairstyle").Texture =
ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Hair, Use16X16Sprites));
GetNode<Sprite2D>("parts/outfit").Texture =
ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Outfit, Use16X16Sprites));
GetNode<Sprite2D>("parts/eye").Texture =
ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Eye, Use16X16Sprites));
GetNode<Sprite2D>("parts/accessory").Texture =
ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Accessory, Use16X16Sprites));
GetNode<Sprite2D>("parts/smartphone").Texture =
ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Phone, Use16X16Sprites));
}
}