Refine student suit theme and make it ready for the next step

This commit is contained in:
wjsjwr 2026-01-18 21:45:05 +08:00
parent 73499fde06
commit 4d51c4b3c8
9 changed files with 189 additions and 50 deletions

View File

@ -95,7 +95,7 @@
},
{
"ActionId": "Administration",
"LocationId": "AdministrationBuilding",
"LocationId": "AdminBuilding",
"DurationSeconds": 5.0,
"HungerDelta": -0.20,
"EnergyDelta": -0.40,

View File

@ -19,6 +19,31 @@ public partial class CampusController : Node2D {
private readonly List<Vector2I> _astarWalkableCells = new();
private readonly List<CampusBehaviorAgent> _behaviorAgents = new();
private readonly CampusBehaviorWorld _behaviorWorld = new();
private readonly Dictionary<CampusLocationId, CampusBuilding> _buildings = new() {
{
CampusLocationId.Laboratory,
new CampusBuilding("Laboratory", new Rect2I(48, 72, 192, 160), new Vector2I(150, 196))
}, {
CampusLocationId.Library,
new CampusBuilding("Library", new Rect2I(312, 64, 256, 160), new Vector2I(440, 196))
}, {
CampusLocationId.Canteen,
new CampusBuilding("Canteen", new Rect2I(640, 64, 128, 96), new Vector2I(745, 166))
}, {
CampusLocationId.Dormitory,
new CampusBuilding("Dormitory", new Rect2I(800, 64, 96, 96), new Vector2I(848, 192))
},
{ CampusLocationId.ArtificialLake, new CampusBuilding("ArtificialLake", new Rect2I(), new Vector2I(943, 179)) },
{ CampusLocationId.CoffeeShop, new CampusBuilding("CoffeeShop", new Rect2I(), new Vector2I(160, 395)) }, {
CampusLocationId.AdminBuilding,
new CampusBuilding("AdminBuilding", new Rect2I(234, 312, 128, 96), new Vector2I(296, 452))
}, {
CampusLocationId.FootballField,
new CampusBuilding("FootballField", new Rect2I(568, 320, 160, 160), new Vector2I(560, 300))
}
};
private readonly List<Vector2> _coveragePoints = new();
private readonly CampusLocationRegistry _locationRegistry = new();
private List<string> _archetypeIds = new();
@ -164,7 +189,7 @@ public partial class CampusController : Node2D {
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_Admin", CampusLocationId.AdminBuilding);
RegisterLocation(locationsRoot, "Location_Field", CampusLocationId.FootballField);
}
@ -813,6 +838,13 @@ public partial class CampusController : Node2D {
}
}
private class CampusBuilding(string locationId, Rect2I pRegion, Vector2I entrance) {
public string LocationId { get; } = locationId;
public HashSet<string> MemberIds { get; set; } = [];
public Rect2I PortraitRegion { get; } = pRegion;
public Vector2I EntrancePoint { get; } = entrance;
}
private sealed partial class DebugGridOverlay : Node2D {
public CampusController CampusOwner { get; set; }

View File

@ -1,10 +1,11 @@
[gd_scene load_steps=9 format=3 uid="uid://b0cu4fa7vohmw"]
[gd_scene load_steps=10 format=3 uid="uid://b0cu4fa7vohmw"]
[ext_resource type="Script" uid="uid://ew4ig6hnrsau" path="res://scenes/CampusController.cs" id="1_controller"]
[ext_resource type="PackedScene" uid="uid://cf6b1t8pujosf" path="res://scenes/ui-elements/log_panel.tscn" id="1_hi2p7"]
[ext_resource type="Texture2D" uid="uid://brmthiu6rxhqc" path="res://res_src/campus.png" id="1_p4tmp"]
[ext_resource type="PackedScene" uid="uid://db2qcx61nc0q4" path="res://scenes/ui-elements/top-bar.tscn" id="2_p4tmp"]
[ext_resource type="PackedScene" uid="uid://drmjsqoy8htc8" path="res://scenes/ui-elements/task_list.tscn" id="3_4gjr3"]
[ext_resource type="PackedScene" uid="uid://bmx4ukf3rmoi7" path="res://scenes/student_portrait_16_native.tscn" id="6_74kl0"]
[sub_resource type="NavigationPolygon" id="NavigationPolygon_8u8vn"]
vertices = PackedVector2Array(956, 540, 4, 540, 420, 516, 460, 516, 380, 516, 204, 356, 204, 260, 228, 284, 228, 468, 4, 500, 100, 500, 380, 468, 308, 468, 132, 212, 132, 180, 172, 180, 172, 212, 244, 212, 244, 60, 268, 60, 268, 236, 428, 236, 428, 172, 452, 172, 452, 236, 612, 236, 612, 60, 636, 60, 636, 244, 708, 244, 708, 196, 732, 196, 732, 140, 756, 140, 756, 196, 772, 196, 772, 60, 796, 60, 796, 228, 836, 228, 836, 180, 860, 180, 860, 284, 828, 284, 932, 308, 932, 180, 956, 180, 956, 332, 828, 332, 828, 308, 828, 516, 804, 516, 804, 284, 572, 284, 572, 300, 548, 300, 548, 284, 956, 516, 116, 364, 124, 364, 124, 396, 196, 396, 196, 356, 180, 436, 180, 412, 140, 412, 284, 468, 284, 436, 308, 436, 140, 436, 4, 284, 4, 60, 28, 60, 28, 212, 100, 284, 116, 260, 460, 284, 420, 284)
@ -2033,6 +2034,7 @@ visible = false
tile_set = SubResource("TileSet_74kl0")
[node name="Locations" type="Node2D" parent="."]
visible = false
[node name="Location_Lab" type="Node2D" parent="Locations"]
position = Vector2(150, 196)
@ -2057,3 +2059,16 @@ position = Vector2(296, 452)
[node name="Location_Field" type="Node2D" parent="Locations"]
position = Vector2(560, 300)
[node name="PortraitContainer" type="Node" parent="."]
[node name="HFlowContainer" type="HFlowContainer" parent="PortraitContainer"]
visible = false
offset_left = 568.0
offset_top = 320.0
offset_right = 728.0
offset_bottom = 480.0
[node name="StudentPortrait2" parent="PortraitContainer/HFlowContainer" instance=ExtResource("6_74kl0")]
[node name="StudentPortrait" parent="PortraitContainer/HFlowContainer" instance=ExtResource("6_74kl0")]

View File

@ -908,7 +908,7 @@ public sealed class CampusBehaviorAgent {
CampusLocationId.Dormitory => "宿舍",
CampusLocationId.ArtificialLake => "人工湖",
CampusLocationId.CoffeeShop => "咖啡店",
CampusLocationId.AdministrationBuilding => "行政楼",
CampusLocationId.AdminBuilding => "行政楼",
CampusLocationId.FootballField => "足球场",
CampusLocationId.RandomWander => "校园",
_ => "校园"

View File

@ -18,7 +18,7 @@ public enum CampusLocationId {
Dormitory, // 宿舍
ArtificialLake, // 人工湖
CoffeeShop, // 咖啡店
AdministrationBuilding, // 行政楼
AdminBuilding, // 行政楼
FootballField, // 足球场
RandomWander // 随机漫游
}
@ -110,7 +110,7 @@ public sealed class CampusBehaviorConfig {
public CampusActionConfig GetActionConfig(CampusActionId id) {
if (_actionLookup.Count == 0) BuildLookup();
return _actionLookup.TryGetValue(id, out var config) ? config : null;
return _actionLookup.GetValueOrDefault(id);
}
private void BuildLookup() {

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using Godot;
using Views;
/// <summary>
/// 校园学生角色控制器
@ -12,6 +13,19 @@ public partial class CampusStudent : CharacterBody2D {
/// </summary>
private readonly List<Vector2> _gridPath = new();
/// <summary>
/// 外观主题
/// </summary>
private readonly SuitTheme _theme = new();
/// <summary>
/// 获取外观主题ID
/// </summary>
/// <returns>主题ID</returns>
public ulong GetSuitThemeId() {
return _theme.GetThemeId();
}
/// <summary>
/// 动画播放器
/// </summary>
@ -112,10 +126,6 @@ public partial class CampusStudent : CharacterBody2D {
/// </summary>
private float _stuckTimer;
private Sprite2D[] _theme;
private ulong _themeId;
/// <summary>
/// 是否使用物理移动
/// </summary>
@ -181,6 +191,8 @@ public partial class CampusStudent : CharacterBody2D {
public override void _Ready() {
_animationPlayer = GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
_campusController = GetTree()?.CurrentScene as CampusController;
_theme.IsPortrait = false;
_theme.Use16 = Use16X16Sprites;
CacheSprites();
ConfigureCollision();
@ -377,16 +389,12 @@ public partial class CampusStudent : CharacterBody2D {
/// </summary>
public void ApplyRandomTheme() {
// 随机替换身体与配件贴图,形成不同主题外观
if (_theme == null) CacheSprites();
if (_theme.IsEmpty()) CacheSprites();
Debug.Assert(_theme != null, nameof(_theme) + " != null");
_themeId = 0;
for (var typeId = 0; typeId < (int)Res.Type.ResTypeMax; typeId++) {
var ret = Res.GetResourcePathWithId(0, (Res.Type)typeId, Use16X16Sprites, false, true);
_theme[typeId].Texture = ResourceLoader.Load<Texture2D>(ret.Path);
_themeId |= (ret.Id & 0xff) << (8 * typeId);
}
for (Res.Type typeId = 0; typeId < Res.Type.ResTypeMax; typeId++)
_theme.ApplyComponent(typeId, Res.GetResourcePathWithId(0, typeId, Use16X16Sprites, false, true));
}
/// <summary>
@ -394,15 +402,12 @@ public partial class CampusStudent : CharacterBody2D {
/// </summary>
private void CacheSprites() {
// 缓存子节点引用,避免每帧查找
// 顺序需要和Res.Type的枚举顺序一致
_theme = [
GetNode<Sprite2D>("parts/accessory"),
GetNode<Sprite2D>("parts/body"),
GetNode<Sprite2D>("parts/eye"),
GetNode<Sprite2D>("parts/hairstyle"),
GetNode<Sprite2D>("parts/outfit"),
GetNode<Sprite2D>("parts/smartphone")
];
_theme.CacheComponent(Res.Type.Accessory, GetNode<Sprite2D>("parts/accessory"));
_theme.CacheComponent(Res.Type.Body, GetNode<Sprite2D>("parts/body"));
_theme.CacheComponent(Res.Type.Eye, GetNode<Sprite2D>("parts/eye"));
_theme.CacheComponent(Res.Type.Hair, GetNode<Sprite2D>("parts/hairstyle"));
_theme.CacheComponent(Res.Type.Outfit, GetNode<Sprite2D>("parts/outfit"));
_theme.CacheComponent(Res.Type.Phone, GetNode<Sprite2D>("parts/smartphone"));
}
/// <summary>

View File

@ -273,6 +273,12 @@ public partial class Lab : Node2D {
return path;
}
/// <summary>
/// 获取指定类型的地图块位置
/// </summary>
/// <param name="nType">地图节点类型</param>
/// <param name="idx">索引</param>
/// <returns>地图块位置</returns>
public Vector2I GetTypedBlock(MapNodeType nType, uint idx) {
switch (nType) {
default:

View File

@ -1,30 +1,22 @@
using System.Collections.Generic;
using Godot;
using Views;
public partial class StudentPortrait16Native : Node2D {
/// <summary>
/// 饰品精灵
/// </summary>
private Sprite2D _accessory;
/// <summary>
/// 动画播放器
/// </summary>
private AnimationPlayer _animationPlayer;
/// <summary>
/// 身体精灵
/// </summary>
private Sprite2D _body;
/// <summary>
/// 眼睛精灵
/// </summary>
private Sprite2D _eye;
/// <summary>
/// 发型精灵
/// </summary>
private Sprite2D _hairstyle;
private readonly SuitTheme _theme = new();
/// <summary>
/// 主题ID
/// </summary>
public ulong ThemeId {
get => _theme.GetThemeId();
set => _theme.ApplyTheme(value);
}
// Called when the node enters the scene tree for the first time.
public override void _Ready() {
@ -41,10 +33,10 @@ public partial class StudentPortrait16Native : Node2D {
/// </summary>
private void CacheSprites() {
// 缓存子节点引用,避免每帧查找
_body = GetNode<Sprite2D>("parts/body");
_hairstyle = GetNode<Sprite2D>("parts/hairstyle");
_eye = GetNode<Sprite2D>("parts/eye");
_accessory = GetNode<Sprite2D>("parts/accessory");
_theme.CacheComponent(Res.Type.Accessory, GetNode<Sprite2D>("parts/accessory"));
_theme.CacheComponent(Res.Type.Body, GetNode<Sprite2D>("parts/body"));
_theme.CacheComponent(Res.Type.Eye, GetNode<Sprite2D>("parts/eye"));
_theme.CacheComponent(Res.Type.Hair, GetNode<Sprite2D>("parts/hairstyle"));
}
/// <summary>
@ -62,7 +54,7 @@ public partial class StudentPortrait16Native : Node2D {
/// </summary>
/// <param name="animationName">动画名称</param>
private void OnAnimationFinished(StringName animationName) {
_animationPlayer?.Play(animationName);
_animationPlayer?.Play(animationName);
}
// Called every frame. 'delta' is the elapsed time since the previous frame.

View File

@ -0,0 +1,89 @@
using System.Collections.Generic;
using Godot;
namespace Views;
/// <summary>
/// 套装主题管理类
/// </summary>
public class SuitTheme {
private readonly Dictionary<Res.Type, Sprite2D> _sprites = new();
private readonly Dictionary<Res.Type, ushort> _theme = new();
/// <summary>
/// 是否使用16x16精灵
/// </summary>
public bool Use16 { get; set; }
/// <summary>
/// 是否为头像
/// </summary>
public bool IsPortrait { get; set; }
/// <summary>
/// 缓存组件精灵
/// </summary>
/// <param name="type">资源类型</param>
/// <param name="sprite">精灵对象</param>
public void CacheComponent(Res.Type type, Sprite2D sprite) {
_sprites[type] = sprite;
if (_theme.TryGetValue(type, out var value)) {
sprite.Texture = ResourceLoader.Load<Texture2D>(Res.GetResourcePathWithId(value, type).Path);
}
}
/// <summary>
/// 解析主题ID
/// </summary>
/// <param name="themeId">主题ID</param>
private void _extractThemeId(ulong themeId) {
for (Res.Type typeId = 0; typeId < Res.Type.ResTypeMax; typeId++)
_theme[typeId] = (ushort)((themeId >> (8 * (int)typeId)) & 0xfful);
}
/// <summary>
/// 获取主题ID
/// </summary>
/// <returns>主题ID</returns>
public ulong GetThemeId() {
ulong result = 0;
for (Res.Type typeId = 0; typeId < Res.Type.ResTypeMax; typeId++) {
if (!_theme.TryGetValue(typeId, out var value)) continue;
result |= (ulong)(value & 0xff) << (8 * (int)typeId);
}
return result;
}
/// <summary>
/// 应用主题
/// </summary>
/// <param name="themeId">主题ID</param>
public void ApplyTheme(ulong themeId) {
_extractThemeId(themeId);
foreach (var kvp in _sprites)
if (_theme.TryGetValue(kvp.Key, out var value))
kvp.Value.Texture = ResourceLoader.Load<Texture2D>(
Res.GetResourcePathWithId(value, kvp.Key, Use16, IsPortrait).Path
);
}
/// <summary>
/// 应用组件资源
/// </summary>
/// <param name="type">资源类型</param>
/// <param name="resPath">资源路径对象</param>
public void ApplyComponent(Res.Type type, Res.ResPathWithId resPath) {
if (!_sprites.TryGetValue(type, out var value)) return;
value.Texture = ResourceLoader.Load<Texture2D>(resPath.Path);
_theme[type] = (ushort)resPath.Id;
}
/// <summary>
/// 是否为空
/// </summary>
/// <returns>如果是空则返回true</returns>
public bool IsEmpty() {
return _sprites.Count == 0;
}
}