Skill progression system [Draft] (#1056)

* fuck all

* clean up

* remove knowledge books

* Update base.yml

* cl

* fix #991

* fix

* Update subgamemodes.yml

* manual migration + recipes fix

* Update comoss.yml

* setup data

* setup skills systems

* more blacksmithing skills

* Update base.yml

* Create CP14SkillEffectAction.cs

* skill button returns

* base graph UI window

* skill tree drawing

* UI improve

* pyro setup

* skill trees selection, dragging control

* refactor skill system: rename Skills to LearnedSkills, add experience tracking and learning logic

* Create available.png

* skill description generation setup

* auto parsing skill names and descriptions

* Hydrosophistry

* water light fire and metamagic ported to skill tre

* ice dagger spell returns

* Update ice_dagger.yml

* delete old files

* Update modular_garde.yml

* ice arrow spell

* link graph with parallax

* show experience counter

* polish

* puf

* p

* pipap

* finally ready to merg?

* fix

* fix 2
This commit is contained in:
Ed
2025-03-27 17:20:20 +03:00
committed by GitHub
parent 6a6350a827
commit 1e36a61339
159 changed files with 2283 additions and 1863 deletions

View File

@@ -118,7 +118,7 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction MappingOpenContextMenu = "MappingOpenContextMenu";
//CP14 keys
public static readonly BoundKeyFunction CP14OpenKnowledgeMenu = "CP14OpenKnowledgeMenu";
public static readonly BoundKeyFunction CP14OpenSkillMenu = "CP14OpenSkillMenu";
//CP14 keys end
}
}

View File

@@ -1,3 +1,5 @@
using Content.Shared._CP14.Skill.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
@@ -50,4 +52,10 @@ public sealed partial class LoadoutPrototype : IPrototype, IEquipmentLoadout
/// </summary>
[DataField]
public List<EntProtoId> Actions { get; set; } = new();
/// <summary>
/// CP14 - it is possible to give skill trees to players who have taken this loadout
/// </summary>
[DataField]
public Dictionary<ProtoId<CP14SkillTreePrototype>, FixedPoint2> SkillTree = new();
}

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Shared._CP14.Skill;
using Content.Shared.Actions;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
@@ -25,6 +26,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly SharedActionsSystem _action = default!; //CP14
[Dependency] private readonly CP14SharedSkillSystem _skill = default!; //CP14
private EntityQuery<HandsComponent> _handsQuery;
private EntityQuery<InventoryComponent> _inventoryQuery;
@@ -90,7 +92,7 @@ public abstract class SharedStationSpawningSystem : EntitySystem
{
EquipStartingGear(entity, loadout.StartingGear, raiseEvent);
EquipStartingGear(entity, (IEquipmentLoadout) loadout, raiseEvent);
CP14EquipStartingActions(entity, loadout);
CP14EquipStartingActions(entity, loadout); //CP14
}
private void CP14EquipStartingActions(EntityUid entity, LoadoutPrototype loadout)
@@ -99,6 +101,11 @@ public abstract class SharedStationSpawningSystem : EntitySystem
{
_action.AddAction(entity, action);
}
foreach (var tree in loadout.SkillTree)
{
_skill.TryAddExperience(entity, tree.Key, tree.Value);
}
}
/// <summary>

View File

@@ -96,10 +96,10 @@ namespace Content.Shared.Verbs
public static readonly VerbCategory CP14CurrencyConvert = new("cp14-verb-categories-currency-converter", null); //CP14
public static readonly VerbCategory CP14KnowledgeAdd = new("cp14-verb-categories-knowledge-add", null); //CP14
public static readonly VerbCategory CP14AdminSkillAdd =
new ("cp14-verb-categories-admin-skill-add", null, iconsOnly: true) { Columns = 6 }; //CP14
public static readonly VerbCategory CP14KnowledgeRemove = new("cp14-verb-categories-knowledge-remove", null); //CP14
public static readonly VerbCategory CP14KnowledgeLearn = new("cp14-verb-categories-knowledge-learn", null); //CP14
public static readonly VerbCategory CP14AdminSkillRemove =
new ("cp14-verb-categories-admin-skill-remove", null, iconsOnly: true) { Columns = 6 }; //CP14
}
}

View File

@@ -1,62 +0,0 @@
using System.Text;
using Content.Shared._CP14.Knowledge.Components;
using Content.Shared.Paper;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Knowledge;
public sealed class CP14KnowledgePaperTextSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly PaperSystem _paper = default!;
public override void Initialize()
{
SubscribeLocalEvent<CP14KnowledgePaperTextComponent, MapInitEvent>(OnPaperMapInit);
}
private void OnPaperMapInit(Entity<CP14KnowledgePaperTextComponent> ent, ref MapInitEvent args)
{
if (!TryComp<PaperComponent>(ent, out var paperComponent))
return;
var paper = new Entity<PaperComponent>(ent, paperComponent);
if (!TryComp<CP14KnowledgeLearningSourceComponent>(ent, out var knowledge))
return;
if (knowledge.Knowledge.Count == 0)
return;
var content = GenerateText(knowledge);
_paper.SetContent(paper, content);
paper.Comp.EditingDisabled = true;
// Yes we need to do synchronization after such changes,
// yes predict, but we need to
Dirty(paper);
}
private string GenerateText(CP14KnowledgeLearningSourceComponent source)
{
var stringBuilder = new StringBuilder();
// Header
stringBuilder.Append(Loc.GetString("cp14-knowledge-book-pre-text"));
// Main body
foreach (var prototypeId in source.Knowledge)
{
if (!_prototype.TryIndex(prototypeId, out var indexedKnowledge))
continue;
stringBuilder.Append($"\n{Loc.GetString(indexedKnowledge.Desc)}");
}
// Footer
stringBuilder.Append($"\n\n{Loc.GetString("cp14-knowledge-book-post-text")}");
return stringBuilder.ToString();
}
}

View File

@@ -1,6 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared._CP14.Knowledge.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class CP14AllKnowingComponent : Component;

View File

@@ -1,15 +0,0 @@
using Content.Shared._CP14.Knowledge.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Knowledge.Components;
/// <summary>
/// The ability to add a <see cref="CP14KnowledgePrototype"/> to an entity
/// and quickly teach it some skills.
/// </summary>
[RegisterComponent, Access(typeof(SharedCP14KnowledgeSystem))]
public sealed partial class CP14AutoAddKnowledgeComponent : Component
{
[DataField]
public List<ProtoId<CP14KnowledgePrototype>> Knowledge = [];
}

View File

@@ -1,17 +0,0 @@
using Content.Shared._CP14.Knowledge.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Knowledge.Components;
/// <summary>
/// Allows new knowledge to be learnt through interactions with an object.
/// </summary>
[RegisterComponent, Access(typeof(SharedCP14KnowledgeSystem))]
public sealed partial class CP14KnowledgeLearningSourceComponent : Component
{
[DataField, ViewVariables(VVAccess.ReadOnly)]
public HashSet<ProtoId<CP14KnowledgePrototype>> Knowledge { get; private set; } = [];
[DataField]
public TimeSpan DoAfter = TimeSpan.FromSeconds(5f);
}

View File

@@ -1,8 +0,0 @@
namespace Content.Shared._CP14.Knowledge.Components;
/// <summary>
/// Automatically generates content for PaperComponent,
/// based on the knowledge that can be learnt from this object.
/// </summary>
[RegisterComponent, Access(typeof(SharedCP14KnowledgeSystem))]
public sealed partial class CP14KnowledgePaperTextComponent : Component;

View File

@@ -1,14 +0,0 @@
using Content.Shared._CP14.Knowledge.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Knowledge.Components;
/// <summary>
/// A list of <see cref="CP14KnowledgePrototype"/> learned by this entity.
/// </summary>
[RegisterComponent, Access(typeof(SharedCP14KnowledgeSystem))]
public sealed partial class CP14KnowledgeStorageComponent : Component
{
[DataField, ViewVariables(VVAccess.ReadOnly)]
public HashSet<ProtoId<CP14KnowledgePrototype>> Knowledge { get; private set; } = [];
}

View File

@@ -1,18 +0,0 @@
using Content.Shared._CP14.Knowledge.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.Knowledge.Events;
[Serializable, NetSerializable]
public sealed class CP14KnowledgeInfoEvent : EntityEventArgs
{
public readonly NetEntity NetEntity;
public readonly HashSet<ProtoId<CP14KnowledgePrototype>> AllKnowledge;
public CP14KnowledgeInfoEvent(NetEntity netEntity, HashSet<ProtoId<CP14KnowledgePrototype>> allKnowledge)
{
NetEntity = netEntity;
AllKnowledge = allKnowledge;
}
}

View File

@@ -1,17 +0,0 @@
using Content.Shared._CP14.Knowledge.Prototypes;
using Content.Shared.DoAfter;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.Knowledge.Events;
[Serializable, NetSerializable]
public sealed partial class CP14KnowledgeLearnDoAfterEvent : DoAfterEvent
{
public ProtoId<CP14KnowledgePrototype> Knowledge;
public override DoAfterEvent Clone()
{
return this;
}
}

View File

@@ -1,18 +0,0 @@
using Content.Shared._CP14.Knowledge.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Knowledge.Events;
public sealed class CP14KnowledgeUsedEvent : EntityEventArgs
{
public readonly EntityUid User;
public readonly ProtoId<CP14KnowledgePrototype> Knowledge;
public readonly float Factor;
public CP14KnowledgeUsedEvent(EntityUid uid, ProtoId<CP14KnowledgePrototype> knowledge, float factor)
{
User = uid;
Knowledge = knowledge;
Factor = factor;
}
}

View File

@@ -1,14 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.Knowledge.Events;
[Serializable, NetSerializable]
public sealed class CP14RequestKnowledgeInfoEvent : EntityEventArgs
{
public readonly NetEntity NetEntity;
public CP14RequestKnowledgeInfoEvent(NetEntity netEntity)
{
NetEntity = netEntity;
}
}

View File

@@ -1,25 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Knowledge.Prototypes;
/// <summary>
/// Abstract knowledge that may be required to use items or crafts.
/// </summary>
[Prototype("CP14Knowledge")]
public sealed partial class CP14KnowledgePrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
[DataField(required: true)]
public LocId Name { get; private set; }
[DataField]
public LocId Desc { get; private set; }
/// <summary>
/// To study this knowledge, other knowledge on which it is based may be necessary.
/// </summary>
[DataField]
public HashSet<ProtoId<CP14KnowledgePrototype>> Dependencies = [];
}

View File

@@ -1,31 +0,0 @@
using Content.Shared._CP14.Knowledge.Components;
using Content.Shared._CP14.Knowledge.Events;
using Content.Shared._CP14.Knowledge.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Knowledge;
public abstract class SharedCP14KnowledgeSystem : EntitySystem
{
public bool HasKnowledge(Entity<CP14KnowledgeStorageComponent?> entity, ProtoId<CP14KnowledgePrototype> knowledge)
{
if (HasComp<CP14AllKnowingComponent>(entity))
return true;
return Resolve(entity, ref entity.Comp, false) && entity.Comp.Knowledge.Contains(knowledge);
}
public bool TryUseKnowledge(Entity<CP14KnowledgeStorageComponent?> entity, ProtoId<CP14KnowledgePrototype> knowledge, float factor = 1f)
{
if (!Resolve(entity, ref entity.Comp, false))
return false;
if (!entity.Comp.Knowledge.Contains(knowledge))
return false;
var ev = new CP14KnowledgeUsedEvent(entity, knowledge, factor);
RaiseLocalEvent(entity, ev);
return true;
}
}

View File

@@ -0,0 +1,230 @@
using Content.Shared._CP14.Skill.Components;
using Content.Shared._CP14.Skill.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill;
public abstract partial class CP14SharedSkillSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
InitializeAdmin();
}
/// <summary>
/// Directly adds the skill to the player, bypassing any checks.
/// </summary>
public bool TryAddSkill(EntityUid target,
ProtoId<CP14SkillPrototype> skill,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
if (component.LearnedSkills.Contains(skill))
return false;
if (!_proto.TryIndex(skill, out var indexedSkill))
return false;
if (indexedSkill.Effect is not null)
{
indexedSkill.Effect.AddSkill(EntityManager, target);
}
component.SkillsSumExperience += indexedSkill.LearnCost;
component.LearnedSkills.Add(skill);
Dirty(target, component);
return true;
}
/// <summary>
/// Removes the skill from the player, bypassing any checks.
/// </summary>
public bool TryRemoveSkill(EntityUid target,
ProtoId<CP14SkillPrototype> skill,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
if (!component.LearnedSkills.Remove(skill))
return false;
if (!_proto.TryIndex(skill, out var indexedSkill))
return false;
if (indexedSkill.Effect is not null)
{
indexedSkill.Effect.RemoveSkill(EntityManager, target);
}
component.SkillsSumExperience -= indexedSkill.LearnCost;
Dirty(target, component);
return true;
}
/// <summary>
/// Checks if the player has the skill.
/// </summary>
public bool HaveSkill(EntityUid target,
ProtoId<CP14SkillPrototype> skill,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
return component.LearnedSkills.Contains(skill);
}
/// <summary>
/// Adds experience to the specified skill tree for the player.
/// </summary>
public bool TryAddExperience(EntityUid target,
ProtoId<CP14SkillTreePrototype> tree,
FixedPoint2 exp,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
if (component.Progress.TryGetValue(tree, out var currentExp))
{
// If the tree already exists, add experience to it
component.Progress[tree] = currentExp + exp;
}
else
{
// If the tree doesn't exist, initialize it with the experience
component.Progress[tree] = exp;
}
Dirty(target, component);
return true;
}
/// <summary>
/// Removes experience from the specified skill tree for the player.
/// </summary>
public bool TryRemoveExperience(EntityUid target,
ProtoId<CP14SkillTreePrototype> tree,
FixedPoint2 exp,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
if (!component.Progress.TryGetValue(tree, out var currentExp))
return false;
if (currentExp < exp)
return false;
component.Progress[tree] = FixedPoint2.Max(0, component.Progress[tree] - exp);
Dirty(target, component);
return true;
}
/// <summary>
/// Checks if the player can learn the specified skill.
/// </summary>
public bool CanLearnSkill(EntityUid target,
ProtoId<CP14SkillPrototype> skill,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
if (!_proto.TryIndex(skill, out var indexedSkill))
return false;
//Already learned
if (HaveSkill(target, skill, component))
return false;
//Check max cap
if (component.SkillsSumExperience + indexedSkill.LearnCost >= component.ExperienceMaxCap)
return false;
//Prerequisite check
foreach (var prerequisite in indexedSkill.Prerequisites)
{
if (!HaveSkill(target, prerequisite, component))
return false;
}
//Experience check
if (!component.Progress.TryGetValue(indexedSkill.Tree, out var currentExp))
return false;
if (currentExp < indexedSkill.LearnCost)
return false;
return true;
}
/// <summary>
/// Tries to learn the specified skill for the player.
/// </summary>
public bool TryLearnSkill(EntityUid target,
ProtoId<CP14SkillPrototype> skill,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
if (!_proto.TryIndex(skill, out var indexedSkill))
return false;
if (!CanLearnSkill(target, skill, component))
return false;
if (!TryRemoveExperience(target, indexedSkill.Tree, indexedSkill.LearnCost, component))
return false;
if (!TryAddSkill(target, skill, component))
return false;
return false;
}
/// <summary>
/// Helper function to get the skill name for a given skill prototype.
/// </summary>
public string GetSkillName(ProtoId<CP14SkillPrototype> skill)
{
if (!_proto.TryIndex(skill, out var indexedSkill))
return string.Empty;
if (indexedSkill.Name != null)
return Loc.GetString(indexedSkill.Name);
if (indexedSkill.Effect != null)
return indexedSkill.Effect.GetName(EntityManager, _proto) ?? string.Empty;
return string.Empty;
}
/// <summary>
/// Helper function to get the skill description for a given skill prototype.
/// </summary>
public string GetSkillDescription(ProtoId<CP14SkillPrototype> skill)
{
if (!_proto.TryIndex(skill, out var indexedSkill))
return string.Empty;
if (indexedSkill.Desc != null)
return Loc.GetString(indexedSkill.Desc);
if (indexedSkill.Effect != null)
return indexedSkill.Effect.GetDescription(EntityManager, _proto) ?? string.Empty;
return string.Empty;
}
}

View File

@@ -0,0 +1,67 @@
using Content.Shared._CP14.Skill.Components;
using Content.Shared._CP14.Skill.Prototypes;
using Content.Shared.Administration;
using Content.Shared.Administration.Managers;
using Content.Shared.Verbs;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill;
public abstract partial class CP14SharedSkillSystem
{
[Dependency] private readonly ISharedAdminManager _admin = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private void InitializeAdmin()
{
SubscribeLocalEvent<CP14SkillStorageComponent, GetVerbsEvent<Verb>>(OnGetAdminVerbs);
}
private void OnGetAdminVerbs(Entity<CP14SkillStorageComponent> ent, ref GetVerbsEvent<Verb> args)
{
if (!_admin.HasAdminFlag(args.User, AdminFlags.Admin))
return;
var target = args.Target;
//Add Skill
foreach (var skill in _proto.EnumeratePrototypes<CP14SkillPrototype>())
{
if (ent.Comp.LearnedSkills.Contains(skill))
continue;
var name = Loc.GetString(GetSkillName(skill));
args.Verbs.Add(new Verb
{
Text = name,
Message = name + ": " + Loc.GetString(GetSkillDescription(skill)),
Category = VerbCategory.CP14AdminSkillAdd,
Icon = skill.Icon,
Act = () =>
{
TryAddSkill(target, skill);
},
});
}
//Remove Skill
foreach (var skill in ent.Comp.LearnedSkills)
{
if (!_proto.TryIndex(skill, out var indexedSkill))
continue;
var name = Loc.GetString(GetSkillName(skill));
args.Verbs.Add(new Verb
{
Text = name,
Message = name + ": " + Loc.GetString(GetSkillDescription(skill)),
Category = VerbCategory.CP14AdminSkillRemove,
Icon = indexedSkill.Icon,
Act = () =>
{
TryRemoveSkill(target, skill);
},
});
}
}
}

View File

@@ -0,0 +1,48 @@
using Content.Shared._CP14.Skill.Components;
using Content.Shared._CP14.Skill.Prototypes;
using Content.Shared.Bed.Sleep;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill;
public abstract partial class CP14SharedSkillSystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
public void GiveExperienceInRadius(EntityCoordinates position, ProtoId<CP14SkillTreePrototype> tree, FixedPoint2 points, float radius = 5)
{
var entities = _lookup.GetEntitiesInRange<CP14SkillStorageComponent>(position, radius, LookupFlags.Uncontained);
foreach (var ent in entities)
{
//Cant learn if the position is not in range or obstructed
if (!_examine.InRangeUnOccluded(ent, position, radius))
continue;
//Cant learn when dead
if (TryComp<MobStateComponent>(ent, out var mobState) && !_mobState.IsAlive(ent, mobState))
continue;
//Cant learn if the entity is sleeping
if (HasComp<SleepingComponent>(ent))
continue;
TryAddExperience(ent, tree, points);
}
}
public void GiveExperienceInRadius(EntityUid uid,
ProtoId<CP14SkillTreePrototype> tree,
FixedPoint2 points,
float radius = 5)
{
GiveExperienceInRadius(Transform(uid).Coordinates, tree, points, radius);
}
}

View File

@@ -0,0 +1,46 @@
using Content.Shared._CP14.Skill.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.Skill.Components;
/// <summary>
/// Component that stores the skills learned by a player and their progress in the skill trees.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(CP14SharedSkillSystem))]
public sealed partial class CP14SkillStorageComponent : Component
{
[DataField, AutoNetworkedField]
public List<ProtoId<CP14SkillPrototype>> LearnedSkills = new();
/// <summary>
/// The number of experience points spent on skills. Technically this could be calculated via LearnedSkills, but this is a cached value for optimization.
/// </summary>
[DataField, AutoNetworkedField]
public FixedPoint2 SkillsSumExperience = 0;
/// <summary>
/// Keeps track of progress points in the knowledge areas available to the player. Important: The absence of a specific area means that the player CANNOT progress in that area.
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<ProtoId<CP14SkillTreePrototype>, FixedPoint2> Progress = new();
/// <summary>
/// The maximum ceiling of experience points that can be spent on learning skills. Not tied to a category.
/// </summary>
[DataField, AutoNetworkedField]
public FixedPoint2 ExperienceMaxCap = 10;
}
/// <summary>
/// Raised when a player attempts to learn a skill. This is sent from the client to the server.
/// </summary>
[Serializable, NetSerializable]
public sealed class CP14TryLearnSkillMessage(NetEntity entity, ProtoId<CP14SkillPrototype> skill) : EntityEventArgs
{
public readonly NetEntity Entity = entity;
public readonly ProtoId<CP14SkillPrototype> Skill = skill;
}

View File

@@ -0,0 +1,64 @@
using System.Numerics;
using Content.Shared._CP14.Skill.Specials;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared._CP14.Skill.Prototypes;
/// <summary>
/// A skill that can be learned by the player. Skills are grouped into trees, and each skill has a cost to learn, prerequisites, and an effect.
/// </summary>
[Prototype("cp14Skill")]
public sealed partial class CP14SkillPrototype : IPrototype
{
[IdDataField] public string ID { get; } = default!;
/// <summary>
/// Skill Title. If you leave null, the name will try to generate from Effect.GetName()
/// </summary>
[DataField]
public LocId? Name;
/// <summary>
/// Skill Description. If you leave null, the description will try to generate from Effect.GetDescription()
/// </summary>
[DataField]
public LocId? Desc;
/// <summary>
/// The tree this skill belongs to. This is used to group skills together in the UI.
/// </summary>
[DataField(required: true)]
public ProtoId<CP14SkillTreePrototype> Tree = default!;
/// <summary>
/// The cost to learn this skill. This is used to determine how many progress points are needed to learn the skill.
/// </summary>
[DataField]
public float LearnCost = 1f;
/// <summary>
/// Prerequisites for this skill. This is used to determine if the player can learn the skill. If the player does not have all the prerequisites, they cannot learn the skill.
/// </summary>
[DataField]
public HashSet<ProtoId<CP14SkillPrototype>> Prerequisites = new();
/// <summary>
/// Skill UI position. This is used to determine where the skill will be displayed in the skill tree UI.
/// </summary>
[DataField(required: true)]
public Vector2 SkillUiPosition = default!;
/// <summary>
/// Icon for the skill. This is used to display the skill in the skill tree UI.
/// </summary>
[DataField(required: true)]
public SpriteSpecifier Icon = default!;
/// <summary>
/// Skill effect. This is used to determine what happens when the player learns the skill. If you leave null, the skill will not have any effect.
/// But the presence of the skill itself can affect some systems that check for the presence of certain skills.
/// </summary>
[DataField]
public CP14SkillEffect? Effect;
}

View File

@@ -0,0 +1,44 @@
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared._CP14.Skill.Prototypes;
/// <summary>
/// A group of skills combined into one “branch”
/// </summary>
[Prototype("cp14SkillTree")]
public sealed partial class CP14SkillTreePrototype : IPrototype
{
[IdDataField] public string ID { get; } = default!;
[DataField(required: true)]
public LocId Name;
[DataField]
public SpriteSpecifier FrameIcon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_CP14/Interface/Skills/default.rsi"), "frame");
[DataField]
public SpriteSpecifier HoveredIcon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_CP14/Interface/Skills/default.rsi"), "hovered");
[DataField]
public SpriteSpecifier SelectedIcon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_CP14/Interface/Skills/default.rsi"), "selected");
[DataField]
public SpriteSpecifier LearnedIcon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_CP14/Interface/Skills/default.rsi"), "learned");
[DataField]
public SpriteSpecifier AvailableIcon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_CP14/Interface/Skills/default.rsi"), "available");
[DataField]
public string Parallax = "AspidParallax";
[DataField]
public LocId? Desc;
[DataField]
public Color Color;
[DataField]
public SoundSpecifier LearnSound = new SoundCollectionSpecifier("CP14LearnSkill");
}

View File

@@ -0,0 +1,52 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill.Specials;
public sealed partial class AddAction : CP14SkillEffect
{
[DataField(required: true)]
public EntProtoId Action;
public override void AddSkill(EntityManager entManager, EntityUid target)
{
var actionsSystem = entManager.System<SharedActionsSystem>();
actionsSystem.AddAction(target, Action);
}
public override void RemoveSkill(EntityManager entManager, EntityUid target)
{
var actionsSystem = entManager.System<SharedActionsSystem>();
foreach (var (uid, _) in actionsSystem.GetActions(target))
{
if (!entManager.TryGetComponent<MetaDataComponent>(uid, out var metaData))
continue;
if (metaData.EntityPrototype == null)
continue;
if (metaData.EntityPrototype != Action)
continue;
actionsSystem.RemoveAction(target, uid);
}
}
public override string? GetName(EntityManager entMagager, IPrototypeManager protoManager)
{
if (!protoManager.TryIndex(Action, out var indexedAction))
return String.Empty;
return indexedAction.Name;
}
public override string? GetDescription(EntityManager entMagager, IPrototypeManager protoManager)
{
if (!protoManager.TryIndex(Action, out var indexedAction))
return String.Empty;
return indexedAction.Description;
}
}

View File

@@ -0,0 +1,17 @@
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill.Specials;
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract partial class CP14SkillEffect
{
public abstract void AddSkill(EntityManager entManager, EntityUid target);
public abstract void RemoveSkill(EntityManager entManager, EntityUid target);
public abstract string? GetName(EntityManager entMagager, IPrototypeManager protoManager);
public abstract string? GetDescription(EntityManager entMagager, IPrototypeManager protoManager);
}

View File

@@ -1,39 +1,46 @@
using Content.Shared._CP14.Knowledge;
using Content.Shared._CP14.Knowledge.Prototypes;
using Content.Shared._CP14.Skill;
using Content.Shared._CP14.Skill.Prototypes;
using Content.Shared._CP14.Workbench.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared._CP14.Workbench.Requirements;
public sealed partial class KnowledgeRequired : CP14WorkbenchCraftRequirement
public sealed partial class SkillRequired : CP14WorkbenchCraftRequirement
{
/// <summary>
/// If the player does not have this knowledge, the recipe will not be displayed in the workbench.
/// </summary>
public override bool HideRecipe { get; set; } = true;
[DataField(required: true)]
public ProtoId<CP14KnowledgePrototype> Knowledge;
public List<ProtoId<CP14SkillPrototype>> Skills = new();
public override bool CheckRequirement(EntityManager entManager,
IPrototypeManager protoManager,
HashSet<EntityUid> placedEntities,
EntityUid user)
EntityUid user,
CP14WorkbenchRecipePrototype recipe)
{
var knowledgeSystem = entManager.System<SharedCP14KnowledgeSystem>();
var knowledgeSystem = entManager.System<CP14SharedSkillSystem>();
return knowledgeSystem.HasKnowledge(user, Knowledge);
var haveAllSkills = true;
foreach (var skill in Skills)
{
if (!knowledgeSystem.HaveSkill(user, skill))
{
haveAllSkills = false;
break;
}
}
return haveAllSkills;
}
public override void PostCraft(EntityManager entManager, HashSet<EntityUid> placedEntities, EntityUid user)
{
var knowledgeSystem = entManager.System<SharedCP14KnowledgeSystem>();
knowledgeSystem.TryUseKnowledge(user, Knowledge);
}
public override string GetRequirementTitle(IPrototypeManager protoManager)
{
return !protoManager.TryIndex(Knowledge, out var indexedKnowledge)
? "Error knowledge"
: $"{Loc.GetString("cp14-knowledge")}: {Loc.GetString(indexedKnowledge.Name)}";
return string.Empty;
}
public override EntityPrototype? GetRequirementEntityView(IPrototypeManager protoManager)
@@ -41,8 +48,8 @@ public sealed partial class KnowledgeRequired : CP14WorkbenchCraftRequirement
return null;
}
public override SpriteSpecifier GetRequirementTexture(IPrototypeManager protoManager)
public override SpriteSpecifier? GetRequirementTexture(IPrototypeManager protoManager)
{
return new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/students-cap.svg.192dpi.png"));
return null;
}
}

View File

@@ -4,6 +4,7 @@
*/
using Content.Shared._CP14.Material;
using Content.Shared._CP14.Workbench.Prototypes;
using Content.Shared.Materials;
using Content.Shared.Stacks;
using Robust.Shared.Prototypes;
@@ -13,13 +14,20 @@ namespace Content.Shared._CP14.Workbench.Requirements;
public sealed partial class MaterialResource : CP14WorkbenchCraftRequirement
{
public override bool HideRecipe { get; set; } = false;
[DataField(required: true)]
public ProtoId<MaterialPrototype> Material;
[DataField]
public int Count = 1;
public override bool CheckRequirement(EntityManager entManager, IPrototypeManager protoManager, HashSet<EntityUid> placedEntities, EntityUid user)
public override bool CheckRequirement(
EntityManager entManager,
IPrototypeManager protoManager,
HashSet<EntityUid> placedEntities,
EntityUid user,
CP14WorkbenchRecipePrototype recipe)
{
var count = 0;
foreach (var ent in placedEntities)

View File

@@ -3,6 +3,7 @@
* https://github.com/space-wizards/space-station-14/blob/master/LICENSE.TXT
*/
using Content.Shared._CP14.Workbench.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -10,6 +11,8 @@ namespace Content.Shared._CP14.Workbench.Requirements;
public sealed partial class ProtoIdResource : CP14WorkbenchCraftRequirement
{
public override bool HideRecipe { get; set; } = false;
[DataField(required: true)]
public EntProtoId ProtoId;
@@ -19,7 +22,8 @@ public sealed partial class ProtoIdResource : CP14WorkbenchCraftRequirement
public override bool CheckRequirement(EntityManager entManager,
IPrototypeManager protoManager,
HashSet<EntityUid> placedEntities,
EntityUid user)
EntityUid user,
CP14WorkbenchRecipePrototype recipe)
{
var indexedIngredients = IndexIngredients(entManager, placedEntities);

View File

@@ -3,6 +3,7 @@
* https://github.com/space-wizards/space-station-14/blob/master/LICENSE.TXT
*/
using Content.Shared._CP14.Workbench.Prototypes;
using Content.Shared.Stacks;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -11,6 +12,8 @@ namespace Content.Shared._CP14.Workbench.Requirements;
public sealed partial class StackResource : CP14WorkbenchCraftRequirement
{
public override bool HideRecipe { get; set; } = false;
[DataField(required: true)]
public ProtoId<StackPrototype> Stack;
@@ -20,7 +23,8 @@ public sealed partial class StackResource : CP14WorkbenchCraftRequirement
public override bool CheckRequirement(EntityManager entManager,
IPrototypeManager protoManager,
HashSet<EntityUid> placedEntities,
EntityUid user)
EntityUid user,
CP14WorkbenchRecipePrototype recipe)
{
var count = 0;
foreach (var ent in placedEntities)

View File

@@ -3,6 +3,7 @@
* https://github.com/space-wizards/space-station-14/blob/master/LICENSE.TXT
*/
using Content.Shared._CP14.Workbench.Prototypes;
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -11,6 +12,8 @@ namespace Content.Shared._CP14.Workbench.Requirements;
public sealed partial class TagResource : CP14WorkbenchCraftRequirement
{
public override bool HideRecipe { get; set; } = false;
[DataField(required: true)]
public ProtoId<TagPrototype> Tag;
@@ -23,7 +26,12 @@ public sealed partial class TagResource : CP14WorkbenchCraftRequirement
[DataField(required: true)]
public SpriteSpecifier? Texture;
public override bool CheckRequirement(EntityManager entManager, IPrototypeManager protoManager, HashSet<EntityUid> placedEntities, EntityUid user)
public override bool CheckRequirement(
EntityManager entManager,
IPrototypeManager protoManager,
HashSet<EntityUid> placedEntities,
EntityUid user,
CP14WorkbenchRecipePrototype recipe)
{
var tagSystem = entManager.System<TagSystem>();

View File

@@ -3,6 +3,7 @@
* https://github.com/space-wizards/space-station-14/blob/master/LICENSE.TXT
*/
using Content.Shared._CP14.Workbench.Prototypes;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -13,6 +14,11 @@ namespace Content.Shared._CP14.Workbench;
[MeansImplicitUse]
public abstract partial class CP14WorkbenchCraftRequirement
{
/// <summary>
/// If true, failure to fulfill this condition will hide recipes from the possible craft workbench menu
/// </summary>
public abstract bool HideRecipe { get; set; }
/// <summary>
/// Here a check is made that the recipe as a whole can be fulfilled at the current moment. Do not add anything that affects gameplay here, and only perform checks here.
/// </summary>
@@ -20,7 +26,8 @@ public abstract partial class CP14WorkbenchCraftRequirement
public abstract bool CheckRequirement(EntityManager entManager,
IPrototypeManager protoManager,
HashSet<EntityUid> placedEntities,
EntityUid user);
EntityUid user,
CP14WorkbenchRecipePrototype recipe);
/// <summary>
/// An event that is triggered after crafting. This is the place to put important things like removing items, spending stacks or other things.