diff --git a/resources/definitions/campus_behavior.json b/resources/definitions/campus_behavior.json index 6935ff9..db9f3c0 100644 --- a/resources/definitions/campus_behavior.json +++ b/resources/definitions/campus_behavior.json @@ -95,7 +95,7 @@ }, { "ActionId": "Administration", - "LocationId": "AdministrationBuilding", + "LocationId": "AdminBuilding", "DurationSeconds": 5.0, "HungerDelta": -0.20, "EnergyDelta": -0.40, diff --git a/scenes/CampusController.cs b/scenes/CampusController.cs index ce77bcc..015eb92 100644 --- a/scenes/CampusController.cs +++ b/scenes/CampusController.cs @@ -19,6 +19,31 @@ public partial class CampusController : Node2D { private readonly List _astarWalkableCells = new(); private readonly List _behaviorAgents = new(); private readonly CampusBehaviorWorld _behaviorWorld = new(); + + private readonly Dictionary _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 _coveragePoints = new(); private readonly CampusLocationRegistry _locationRegistry = new(); private List _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 MemberIds { get; set; } = []; + public Rect2I PortraitRegion { get; } = pRegion; + public Vector2I EntrancePoint { get; } = entrance; + } + private sealed partial class DebugGridOverlay : Node2D { public CampusController CampusOwner { get; set; } diff --git a/scenes/campus.tscn b/scenes/campus.tscn index 7960558..dac44dc 100644 --- a/scenes/campus.tscn +++ b/scenes/campus.tscn @@ -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")] diff --git a/scripts/Campus/CampusBehaviorAgent.cs b/scripts/Campus/CampusBehaviorAgent.cs index b82a4f6..65715dc 100644 --- a/scripts/Campus/CampusBehaviorAgent.cs +++ b/scripts/Campus/CampusBehaviorAgent.cs @@ -908,7 +908,7 @@ public sealed class CampusBehaviorAgent { CampusLocationId.Dormitory => "宿舍", CampusLocationId.ArtificialLake => "人工湖", CampusLocationId.CoffeeShop => "咖啡店", - CampusLocationId.AdministrationBuilding => "行政楼", + CampusLocationId.AdminBuilding => "行政楼", CampusLocationId.FootballField => "足球场", CampusLocationId.RandomWander => "校园", _ => "校园" diff --git a/scripts/Campus/CampusBehaviorConfig.cs b/scripts/Campus/CampusBehaviorConfig.cs index db0512c..3d412b1 100644 --- a/scripts/Campus/CampusBehaviorConfig.cs +++ b/scripts/Campus/CampusBehaviorConfig.cs @@ -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() { diff --git a/scripts/CampusStudent.cs b/scripts/CampusStudent.cs index 7a88de3..9b639f1 100644 --- a/scripts/CampusStudent.cs +++ b/scripts/CampusStudent.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using Godot; +using Views; /// /// 校园学生角色控制器 @@ -12,6 +13,19 @@ public partial class CampusStudent : CharacterBody2D { /// private readonly List _gridPath = new(); + /// + /// 外观主题 + /// + private readonly SuitTheme _theme = new(); + + /// + /// 获取外观主题ID + /// + /// 主题ID + public ulong GetSuitThemeId() { + return _theme.GetThemeId(); + } + /// /// 动画播放器 /// @@ -112,10 +126,6 @@ public partial class CampusStudent : CharacterBody2D { /// private float _stuckTimer; - private Sprite2D[] _theme; - - private ulong _themeId; - /// /// 是否使用物理移动 /// @@ -181,6 +191,8 @@ public partial class CampusStudent : CharacterBody2D { public override void _Ready() { _animationPlayer = GetNodeOrNull("AnimationPlayer"); _campusController = GetTree()?.CurrentScene as CampusController; + _theme.IsPortrait = false; + _theme.Use16 = Use16X16Sprites; CacheSprites(); ConfigureCollision(); @@ -377,16 +389,12 @@ public partial class CampusStudent : CharacterBody2D { /// 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(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)); } /// @@ -394,15 +402,12 @@ public partial class CampusStudent : CharacterBody2D { /// private void CacheSprites() { // 缓存子节点引用,避免每帧查找 - // 顺序需要和Res.Type的枚举顺序一致 - _theme = [ - GetNode("parts/accessory"), - GetNode("parts/body"), - GetNode("parts/eye"), - GetNode("parts/hairstyle"), - GetNode("parts/outfit"), - GetNode("parts/smartphone") - ]; + _theme.CacheComponent(Res.Type.Accessory, GetNode("parts/accessory")); + _theme.CacheComponent(Res.Type.Body, GetNode("parts/body")); + _theme.CacheComponent(Res.Type.Eye, GetNode("parts/eye")); + _theme.CacheComponent(Res.Type.Hair, GetNode("parts/hairstyle")); + _theme.CacheComponent(Res.Type.Outfit, GetNode("parts/outfit")); + _theme.CacheComponent(Res.Type.Phone, GetNode("parts/smartphone")); } /// diff --git a/scripts/Lab.cs b/scripts/Lab.cs index 925c8fd..f69a0ec 100644 --- a/scripts/Lab.cs +++ b/scripts/Lab.cs @@ -273,6 +273,12 @@ public partial class Lab : Node2D { return path; } + /// + /// 获取指定类型的地图块位置 + /// + /// 地图节点类型 + /// 索引 + /// 地图块位置 public Vector2I GetTypedBlock(MapNodeType nType, uint idx) { switch (nType) { default: diff --git a/scripts/StudentPortrait16Native.cs b/scripts/StudentPortrait16Native.cs index 044edf1..2a7c978 100644 --- a/scripts/StudentPortrait16Native.cs +++ b/scripts/StudentPortrait16Native.cs @@ -1,30 +1,22 @@ +using System.Collections.Generic; using Godot; +using Views; public partial class StudentPortrait16Native : Node2D { - /// - /// 饰品精灵 - /// - private Sprite2D _accessory; - /// /// 动画播放器 /// private AnimationPlayer _animationPlayer; - /// - /// 身体精灵 - /// - private Sprite2D _body; - - /// - /// 眼睛精灵 - /// - private Sprite2D _eye; - - /// - /// 发型精灵 - /// - private Sprite2D _hairstyle; + private readonly SuitTheme _theme = new(); + + /// + /// 主题ID + /// + 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 { /// private void CacheSprites() { // 缓存子节点引用,避免每帧查找 - _body = GetNode("parts/body"); - _hairstyle = GetNode("parts/hairstyle"); - _eye = GetNode("parts/eye"); - _accessory = GetNode("parts/accessory"); + _theme.CacheComponent(Res.Type.Accessory, GetNode("parts/accessory")); + _theme.CacheComponent(Res.Type.Body, GetNode("parts/body")); + _theme.CacheComponent(Res.Type.Eye, GetNode("parts/eye")); + _theme.CacheComponent(Res.Type.Hair, GetNode("parts/hairstyle")); } /// @@ -62,7 +54,7 @@ public partial class StudentPortrait16Native : Node2D { /// /// 动画名称 private void OnAnimationFinished(StringName animationName) { - _animationPlayer?.Play(animationName); + _animationPlayer?.Play(animationName); } // Called every frame. 'delta' is the elapsed time since the previous frame. diff --git a/scripts/Views/SuitTheme.cs b/scripts/Views/SuitTheme.cs new file mode 100644 index 0000000..d5c71f9 --- /dev/null +++ b/scripts/Views/SuitTheme.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Godot; + +namespace Views; + +/// +/// 套装主题管理类 +/// +public class SuitTheme { + private readonly Dictionary _sprites = new(); + private readonly Dictionary _theme = new(); + + /// + /// 是否使用16x16精灵 + /// + public bool Use16 { get; set; } + + /// + /// 是否为头像 + /// + public bool IsPortrait { get; set; } + + /// + /// 缓存组件精灵 + /// + /// 资源类型 + /// 精灵对象 + public void CacheComponent(Res.Type type, Sprite2D sprite) { + _sprites[type] = sprite; + if (_theme.TryGetValue(type, out var value)) { + sprite.Texture = ResourceLoader.Load(Res.GetResourcePathWithId(value, type).Path); + } + } + + /// + /// 解析主题ID + /// + /// 主题ID + private void _extractThemeId(ulong themeId) { + for (Res.Type typeId = 0; typeId < Res.Type.ResTypeMax; typeId++) + _theme[typeId] = (ushort)((themeId >> (8 * (int)typeId)) & 0xfful); + } + + /// + /// 获取主题ID + /// + /// 主题ID + 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; + } + + /// + /// 应用主题 + /// + /// 主题ID + 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( + Res.GetResourcePathWithId(value, kvp.Key, Use16, IsPortrait).Path + ); + } + + /// + /// 应用组件资源 + /// + /// 资源类型 + /// 资源路径对象 + public void ApplyComponent(Res.Type type, Res.ResPathWithId resPath) { + if (!_sprites.TryGetValue(type, out var value)) return; + value.Texture = ResourceLoader.Load(resPath.Path); + _theme[type] = (ushort)resPath.Id; + } + + /// + /// 是否为空 + /// + /// 如果是空则返回true + public bool IsEmpty() { + return _sprites.Count == 0; + } +} \ No newline at end of file