已落地 .tres/json 的内容定义入口,并把 ResourceContentSource/JsonContentSource 实现为可用的加载器;同时给出可运行的样例定义文件,默认会在 GameSession.CreateDefault() 中注册加载。

改动说明
  - 实现内容加载器并支持 res:///user:// 路径解析与 JSON 枚举解析:scripts/Core/ContentRegistry.cs
  - 新增 .tres 资源定义接口与样例资源类(学科定义):scripts/Core/ContentResources.cs
  - 默认注册资源/JSON 数据源,启动时自动合并进内容库:scripts/Core/GameSession.cs
  - 样例 .tres 与 JSON 内容定义:resources/definitions/discipline_biology.tres, resources/definitions/disciplines.json, resources/definitions/archetypes.json
  - 当前 .tres 走“扁平字段 + RuleIds”,数值型 Modifier 更适合先用 JSON 落地,后续可以把更多字段迁入资源类。
  - JSON 采用与 Models 定义一致的结构(DefinitionHeader/LocalizedText/ModifierBundle),便于后续扩展。
This commit is contained in:
wjsjwr 2026-01-01 01:59:57 +08:00
parent 9c1593e717
commit 39682f14fe
9 changed files with 506 additions and 2 deletions

View File

@ -0,0 +1,70 @@
[
{
"Header": {
"Id": "core:archetype_grinder",
"Name": {
"Key": "archetype.grinder.name",
"Fallback": "Grinder"
},
"Description": {
"Key": "archetype.grinder.desc",
"Fallback": "They live in the lab and push everything faster."
},
"Tags": [ "archetype" ]
},
"Tiers": [
{
"RequiredCount": 2,
"Modifiers": {
"AttributeModifiers": [
{ "Type": "Activation", "Add": 5, "Multiplier": 1.1 }
],
"RuleIds": [ "rule:grinder_stress_up" ]
}
},
{
"RequiredCount": 4,
"Modifiers": {
"AttributeModifiers": [
{ "Type": "Activation", "Add": 10, "Multiplier": 1.2 }
],
"RuleIds": [ "rule:grinder_overwork" ]
}
}
]
},
{
"Header": {
"Id": "core:archetype_slacker",
"Name": {
"Key": "archetype.slacker.name",
"Fallback": "Slacker"
},
"Description": {
"Key": "archetype.slacker.desc",
"Fallback": "They recover morale but slow down the team."
},
"Tags": [ "archetype" ]
},
"Tiers": [
{
"RequiredCount": 2,
"Modifiers": {
"StatusModifiers": [
{ "Type": "Mood", "Add": 5, "Multiplier": 1.05 }
],
"RuleIds": [ "rule:slacker_relax" ]
}
},
{
"RequiredCount": 4,
"Modifiers": {
"StatusModifiers": [
{ "Type": "Stress", "Add": -5, "Multiplier": 0.9 }
],
"RuleIds": [ "rule:slacker_spread" ]
}
}
]
}
]

View File

@ -0,0 +1,21 @@
[gd_resource type="Resource" script_class="DisciplineDefinitionResource" load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/Core/DisciplineDefinitionResource.cs" id=1]
[resource]
script = ExtResource("1")
Id = "core:discipline_biology_tres"
NameKey = "discipline.biology.name"
NameFallback = "Biology"
DescriptionKey = "discipline.biology.desc"
DescriptionFallback = "Lab-intensive discipline focused on experiments."
IconPath = ""
Tags = [ "discipline" ]
BuffNameKey = "buff.pipette_master.name"
BuffNameFallback = "Pipette Master"
BuffDescriptionKey = "buff.pipette_master.desc"
BuffDescriptionFallback = "Higher lab success, higher stamina cost."
BuffRuleIds = [ "rule:discipline_biology_pipette_master" ]
RolePoolIds = [ "core:role_alchemist", "core:role_lab_rat" ]
ItemPoolIds = [ "core:item_pipette" ]
TaskKeywordIds = [ "task_keyword_lab" ]

View File

@ -0,0 +1,68 @@
[
{
"Header": {
"Id": "core:discipline_economics",
"Name": {
"Key": "discipline.economics.name",
"Fallback": "Economics"
},
"Description": {
"Key": "discipline.economics.desc",
"Fallback": "Money drives everything; interest becomes a core loop."
},
"Tags": [ "discipline" ]
},
"Buff": {
"Name": {
"Key": "buff.capital_flow.name",
"Fallback": "Capital Flow"
},
"Description": {
"Key": "buff.capital_flow.desc",
"Fallback": "Idle funds generate interest each turn."
},
"Modifiers": {
"ResourceModifiers": [
{ "Type": "Money", "Add": 0, "Multiplier": 1.0 }
],
"RuleIds": [ "rule:discipline_economics_interest" ]
}
},
"RolePoolIds": [ "core:role_surveyor", "core:role_orator" ],
"ItemPoolIds": [ "core:item_bloomberg_terminal" ],
"TaskKeywordIds": [ "task_keyword_finance" ]
},
{
"Header": {
"Id": "core:discipline_computer",
"Name": {
"Key": "discipline.computer.name",
"Fallback": "Computer Science"
},
"Description": {
"Key": "discipline.computer.desc",
"Fallback": "Compute-heavy discipline with strong tech output."
},
"Tags": [ "discipline" ]
},
"Buff": {
"Name": {
"Key": "buff.overclock.name",
"Fallback": "Overclock"
},
"Description": {
"Key": "buff.overclock.desc",
"Fallback": "Server power is amplified for AI tasks."
},
"Modifiers": {
"AttributeModifiers": [
{ "Type": "Engineering", "Add": 5, "Multiplier": 1.05 }
],
"RuleIds": [ "rule:discipline_computer_overclock" ]
}
},
"RolePoolIds": [ "core:role_geek", "core:role_coder" ],
"ItemPoolIds": [ "core:item_rtx_cluster" ],
"TaskKeywordIds": [ "task_keyword_ai" ]
}
]

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
using Godot;
using Godot.Collections;
namespace Core;
/// <summary>
/// 资源集合(便于打包多个定义到一个 .tres
/// </summary>
[GlobalClass]
public partial class ContentCollectionResource : Resource, IContentResourceCollection
{
[Export] public Array<Resource> Items { get; set; } = new();
public IEnumerable<IContentResource> GetItems()
{
foreach (var item in Items)
{
if (item is IContentResource content)
{
yield return content;
}
}
}
}

View File

@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using Godot;
using Models;
namespace Core;
@ -103,7 +107,54 @@ public sealed class ResourceContentSource : IContentSource
public IEnumerable<T> LoadAll<T>() where T : class
{
yield break;
foreach (var path in ResourcePaths)
{
if (string.IsNullOrWhiteSpace(path))
{
continue;
}
var resource = ResourceLoader.Load<Resource>(path);
if (resource == null)
{
continue;
}
foreach (var item in ExtractResources<T>(resource))
{
yield return item;
}
}
}
private IEnumerable<T> ExtractResources<T>(Resource resource) where T : class
{
if (resource is IContentResource content)
{
if (content.GetDefinitionType() == typeof(T))
{
if (content.ToDefinition() is T typed)
{
yield return typed;
}
}
yield break;
}
if (resource is IContentResourceCollection collection)
{
foreach (var item in collection.GetItems())
{
if (item.GetDefinitionType() == typeof(T))
{
if (item.ToDefinition() is T typed)
{
yield return typed;
}
}
}
}
}
}
@ -114,15 +165,135 @@ public sealed class JsonContentSource : IContentSource
{
public int Priority { get; }
public List<string> DataPaths { get; } = new();
private readonly JsonSerializerOptions _options;
public JsonContentSource(int priority)
{
Priority = priority;
_options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
_options.Converters.Add(new JsonStringEnumConverter());
}
public IEnumerable<T> LoadAll<T>() where T : class
{
yield break;
foreach (var path in DataPaths)
{
if (string.IsNullOrWhiteSpace(path))
{
continue;
}
var resolvedPath = ResolvePath(path);
if (!File.Exists(resolvedPath))
{
continue;
}
var json = File.ReadAllText(resolvedPath);
if (string.IsNullOrWhiteSpace(json))
{
continue;
}
if (TryDeserializeList(json, out List<T> list))
{
foreach (var item in list)
{
yield return item;
}
continue;
}
if (TryDeserializeEnvelope(json, out JsonContentEnvelope<T> envelope))
{
if (envelope.Items != null)
{
foreach (var item in envelope.Items)
{
yield return item;
}
}
else if (envelope.Item != null)
{
yield return envelope.Item;
}
continue;
}
if (TryDeserializeSingle(json, out T single))
{
yield return single;
}
}
}
private string ResolvePath(string path)
{
if (path.StartsWith("res://") || path.StartsWith("user://"))
{
return ProjectSettings.GlobalizePath(path);
}
return path;
}
private bool TryDeserializeList<T>(string json, out List<T> list) where T : class
{
try
{
list = JsonSerializer.Deserialize<List<T>>(json, _options);
return list != null;
}
catch
{
list = null;
return false;
}
}
private bool TryDeserializeSingle<T>(string json, out T item) where T : class
{
try
{
item = JsonSerializer.Deserialize<T>(json, _options);
return item != null;
}
catch
{
item = null;
return false;
}
}
private bool TryDeserializeEnvelope<T>(string json, out JsonContentEnvelope<T> envelope) where T : class
{
try
{
envelope = JsonSerializer.Deserialize<JsonContentEnvelope<T>>(json, _options);
if (envelope == null)
{
return false;
}
return envelope.Items != null || envelope.Item != null;
}
catch
{
envelope = null;
return false;
}
}
private sealed class JsonContentEnvelope<T> where T : class
{
public string Type { get; set; }
public List<T> Items { get; set; }
public T Item { get; set; }
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
namespace Core;
/// <summary>
/// 资源定义接口(用于 .tres/.res 的内容加载)
/// 设计说明:
/// 1) 资源负责“序列化友好”,模型负责“运行时友好”。
/// 2) 通过 ToDefinition 映射到纯数据模型,保持解耦。
/// 注意事项:
/// - 资源字段尽量使用 Godot 可序列化的基础类型与 Array。
/// 未来扩展:
/// - 可加入验证器,确保 Id/路径等关键字段符合规范。
/// </summary>
public interface IContentResource
{
Type GetDefinitionType();
object ToDefinition();
}
public interface IContentResourceCollection
{
IEnumerable<IContentResource> GetItems();
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using Godot;
using Godot.Collections;
using Models;
namespace Core;
/// <summary>
/// 学科定义资源(先落地一个完整示例)
/// 设计说明:
/// 1) 为避免 .tres 过度嵌套,字段保持扁平化。
/// 2) Buff 规则通过 RuleIds 存储,数值效果可后续用 JSON 增补。
/// 注意事项:
/// - Id 与资源路径应稳定且小写下划线。
/// 未来扩展:
/// - 可补充 Modifiers/AllowedRoles 等字段,进一步丰富配置能力。
/// </summary>
[GlobalClass]
public partial class DisciplineDefinitionResource : Resource, IContentResource
{
// --- Header ---
[Export] public string Id { get; set; }
[Export] public string NameKey { get; set; }
[Export] public string NameFallback { get; set; }
[Export] public string DescriptionKey { get; set; }
[Export] public string DescriptionFallback { get; set; }
[Export] public string IconPath { get; set; }
[Export] public Array<string> Tags { get; set; } = new();
// --- Buff ---
[Export] public string BuffNameKey { get; set; }
[Export] public string BuffNameFallback { get; set; }
[Export] public string BuffDescriptionKey { get; set; }
[Export] public string BuffDescriptionFallback { get; set; }
[Export] public Array<string> BuffRuleIds { get; set; } = new();
// --- Pools ---
[Export] public Array<string> RolePoolIds { get; set; } = new();
[Export] public Array<string> ItemPoolIds { get; set; } = new();
[Export] public Array<string> TaskKeywordIds { get; set; } = new();
public Type GetDefinitionType() => typeof(DisciplineDefinition);
public object ToDefinition()
{
var header = new DefinitionHeader
{
Id = Id,
IconPath = IconPath,
Name = new LocalizedText
{
Key = NameKey,
Fallback = NameFallback
},
Description = new LocalizedText
{
Key = DescriptionKey,
Fallback = DescriptionFallback
}
};
foreach (var tag in Tags)
{
header.Tags.Add(tag);
}
var buff = new DisciplineBuff
{
Name = new LocalizedText
{
Key = BuffNameKey,
Fallback = BuffNameFallback
},
Description = new LocalizedText
{
Key = BuffDescriptionKey,
Fallback = BuffDescriptionFallback
},
Modifiers = new ModifierBundle()
};
foreach (var ruleId in BuffRuleIds)
{
buff.Modifiers.RuleIds.Add(ruleId);
}
var definition = new DisciplineDefinition
{
Header = header,
Buff = buff
};
AddRange(RolePoolIds, definition.RolePoolIds);
AddRange(ItemPoolIds, definition.ItemPoolIds);
AddRange(TaskKeywordIds, definition.TaskKeywordIds);
return definition;
}
private static void AddRange(Array<string> source, List<string> target)
{
foreach (var value in source)
{
target.Add(value);
}
}
}

View File

@ -33,6 +33,15 @@ public sealed class GameSession
public static GameSession CreateDefault()
{
var registry = new ContentRegistry();
var resourceSource = new ResourceContentSource(0);
resourceSource.ResourcePaths.Add("res://resources/definitions/discipline_biology.tres");
registry.RegisterSource(resourceSource);
var jsonSource = new JsonContentSource(10);
jsonSource.DataPaths.Add("res://resources/definitions/disciplines.json");
jsonSource.DataPaths.Add("res://resources/definitions/archetypes.json");
registry.RegisterSource(jsonSource);
var content = registry.BuildDatabase();
var localization = new GodotLocalizationService();
var events = new DomainEventBus();

View File

@ -11,6 +11,9 @@
<Content Include="docs\任务与经济系统.md" />
<Content Include="docs\角色与羁绊系统.md" />
<Content Include="README.md" />
<Content Include="resources\definitions\archetypes.json" />
<Content Include="resources\definitions\disciplines.json" />
<Content Include="resources\definitions\discipline_biology.tres" />
</ItemGroup>
<ItemGroup>
<Folder Include="addons\" />