Skill requirements (#1085)

* prerequites refactor

* tiefling only spells

* metamagic update

* sort trying

* goblin speed port
This commit is contained in:
Ed
2025-03-29 16:50:14 +03:00
committed by GitHub
parent 2248e2ad5c
commit fddbb010cf
32 changed files with 378 additions and 89 deletions

View File

@@ -1,3 +1,4 @@
using Content.Client._CP14.UserInterface.Systems.Skill;
using Content.Client.UserInterface.Systems.Actions;
using Content.Client.UserInterface.Systems.Admin;
using Content.Client.UserInterface.Systems.Bwoink;

View File

@@ -3,6 +3,7 @@ using System.Numerics;
using Content.Shared._CP14.Skill;
using Content.Shared._CP14.Skill.Components;
using Content.Shared._CP14.Skill.Prototypes;
using Content.Shared._CP14.Skill.Restrictions;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -130,19 +131,25 @@ public sealed partial class CP14SkillTreeGraphControl : BoxContainer
var fromPos = skill.SkillUiPosition * GridSize * UIScale + _globalOffset;
foreach (var prerequisite in skill.Prerequisites)
foreach (var req in skill.Restrictions)
{
if (!_proto.TryIndex(prerequisite, out var prerequisiteSkill))
continue;
switch (req)
{
case NeedPrerequisite prerequisite:
if (!_proto.TryIndex(prerequisite.Prerequisite, out var prerequisiteSkill))
continue;
if (prerequisiteSkill.Tree != Tree)
continue;
if (prerequisiteSkill.Tree != Tree)
continue;
var learned = playerSkills.Contains(skill.ID);
var learned = playerSkills.Contains(skill.ID);
var toPos = prerequisiteSkill.SkillUiPosition * GridSize * UIScale + _globalOffset;
var color = learned ? Color.White : Color.FromSrgb(new Color(0.7f, 0.7f, 0.7f));
handle.DrawLine(fromPos, toPos, color);
var toPos = prerequisiteSkill.SkillUiPosition * GridSize * UIScale + _globalOffset;
var color = learned ? Color.White : Color.FromSrgb(new Color(0.7f, 0.7f, 0.7f));
handle.DrawLine(fromPos, toPos, color);
break;
}
}
}
@@ -153,9 +160,7 @@ public sealed partial class CP14SkillTreeGraphControl : BoxContainer
continue;
//TODO: Not optimized, recalculates every frame. Needs to be calculated on state update and cached.
var learned = playerSkills.Contains(skill.ID);
var available = _skillSystem.CanLearnSkill(_player.Value, skill.ID);
var canBeLearned = learned || skill.Prerequisites.All(prerequisite => playerSkills.Contains(prerequisite));
var canBeLearned = _skillSystem.CanLearnSkill(_player.Value, skill, _player.Value.Comp);
var pos = skill.SkillUiPosition * GridSize * UIScale + _globalOffset;
// Base skill icon
@@ -190,6 +195,9 @@ public sealed partial class CP14SkillTreeGraphControl : BoxContainer
handle.DrawTextureRect(hoveredTexture, hoveredQuad, Color.White);
}
var learned = playerSkills.Contains(skill.ID);
var allowedToLearn = _skillSystem.AllowedToLearn(_player.Value, skill, _player.Value.Comp);
// Learned Skill
if (learned)
{
@@ -198,7 +206,7 @@ public sealed partial class CP14SkillTreeGraphControl : BoxContainer
var learnedQuad = new UIBox2(pos - learnedSize / 2, pos + learnedSize / 2);
handle.DrawTextureRect(learnedTexture, learnedQuad, Color.White);
}
else if (available)
else if (canBeLearned)
{
var availableTexture = Tree.AvailableIcon.Frame0();
var availableSize = new Vector2(availableTexture.Width, availableTexture.Height) * 2;
@@ -206,11 +214,12 @@ public sealed partial class CP14SkillTreeGraphControl : BoxContainer
handle.DrawTextureRect(availableTexture, availableQuad, Color.White);
}
var iconColor = Color.White;
if (!learned)
iconColor = Color.FromSrgb(new Color(0.7f, 0.7f, 0.7f));
if (!canBeLearned && !learned)
iconColor = Color.FromSrgb(new Color(0f, 0f, 0f));
var iconColor = allowedToLearn switch
{
true when !learned => Color.FromSrgb(new Color(0.7f, 0.7f, 0.7f)),
false when !learned => Color.FromSrgb(new Color(0f, 0f, 0f)),
_ => Color.White
};
handle.DrawTextureRect(baseTexture, baseQuad, iconColor);
}

View File

@@ -1,5 +1,6 @@
using System.Linq;
using System.Numerics;
using System.Text;
using Content.Client._CP14.Skill;
using Content.Client._CP14.Skill.Ui;
using Content.Client._CP14.UserInterface.Systems.Skill.Window;
@@ -18,7 +19,7 @@ using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Character;
namespace Content.Client._CP14.UserInterface.Systems.Skill;
[UsedImplicitly]
public sealed class CP14SkillUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>,
@@ -26,12 +27,13 @@ public sealed class CP14SkillUIController : UIController, IOnStateEntered<Gamepl
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[UISystemDependency] private readonly CP14ClientSkillSystem _skill = default!;
private CP14SkillWindow? _window;
private CP14SkillPrototype? _selectedSkill;
private MenuButton? SkillButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.CP14SkillButton;
private MenuButton? SkillButton => UIManager.GetActiveUIWidgetOrNull<Client.UserInterface.Systems.MenuBar.Widgets.GameTopMenuBar>()?.CP14SkillButton;
public void OnStateEntered(GameplayState state)
{
@@ -142,8 +144,23 @@ public sealed class CP14SkillUIController : UIController, IOnStateEntered<Gamepl
{
var msg = new FormattedMessage();
if (_player.LocalEntity == null)
return msg;
var sb = new StringBuilder();
//Description
msg.TryAddMarkup(_skill.GetSkillDescription(skill) + "\n", out _);
sb.Append(_skill.GetSkillDescription(skill) + "\n \n");
//Restrictions
foreach (var req in skill.Restrictions)
{
var color = req.Check(_entManager, _player.LocalEntity.Value) ? "green" : "red";
sb.Append($"- [color={color}]{req.GetDescription(_entManager, _proto)}[/color]\n");
}
msg.TryAddMarkup(sb.ToString(), out _);
return msg;
}

View File

@@ -138,11 +138,43 @@ public abstract partial class CP14SharedSkillSystem : EntitySystem
public bool CanLearnSkill(EntityUid target,
ProtoId<CP14SkillPrototype> skill,
CP14SkillStorageComponent? component = null)
{
if (!_proto.TryIndex(skill, out var indexedSkill))
return false;
return CanLearnSkill(target, indexedSkill, component);
}
/// <summary>
/// Checks if the player can learn the specified skill.
/// </summary>
public bool CanLearnSkill(EntityUid target,
CP14SkillPrototype skill,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
if (!_proto.TryIndex(skill, out var indexedSkill))
if (!AllowedToLearn(target, skill, component))
return false;
//Experience check
if (!component.Progress.TryGetValue(skill.Tree, out var currentExp))
return false;
if (currentExp < skill.LearnCost)
return false;
return true;
}
/// <summary>
/// Is it allowed to learn this skill? The player may not have enough points to learn it, but has already met all the requirements to learn it.
/// </summary>
public bool AllowedToLearn(EntityUid target,
CP14SkillPrototype skill,
CP14SkillStorageComponent? component = null)
{
if (!Resolve(target, ref component, false))
return false;
//Already learned
@@ -150,22 +182,16 @@ public abstract partial class CP14SharedSkillSystem : EntitySystem
return false;
//Check max cap
if (component.SkillsSumExperience + indexedSkill.LearnCost >= component.ExperienceMaxCap)
if (component.SkillsSumExperience + skill.LearnCost >= component.ExperienceMaxCap)
return false;
//Prerequisite check
foreach (var prerequisite in indexedSkill.Prerequisites)
//Restrictions check
foreach (var req in skill.Restrictions)
{
if (!HaveSkill(target, prerequisite, component))
if (!req.Check(EntityManager, target))
return false;
}
//Experience check
if (!component.Progress.TryGetValue(indexedSkill.Tree, out var currentExp))
return false;
if (currentExp < indexedSkill.LearnCost)
return false;
return true;
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Shared._CP14.Skill.Components;
using Content.Shared._CP14.Skill.Prototypes;
using Content.Shared.Administration;
@@ -11,21 +12,43 @@ public abstract partial class CP14SharedSkillSystem
{
[Dependency] private readonly ISharedAdminManager _admin = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private IEnumerable<CP14SkillPrototype>? _allSkills;
private void InitializeAdmin()
{
SubscribeLocalEvent<CP14SkillStorageComponent, GetVerbsEvent<Verb>>(OnGetAdminVerbs);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReloaded);
UpdateCachedSkill();
}
private void OnPrototypeReloaded(PrototypesReloadedEventArgs ev)
{
if (!ev.WasModified<CP14SkillPrototype>())
return;
UpdateCachedSkill();
}
private void UpdateCachedSkill()
{
_allSkills = _proto.EnumeratePrototypes<CP14SkillPrototype>().ToList().OrderBy(skill => skill.Tree.Id).ThenBy(skill => skill.Name);
}
private void OnGetAdminVerbs(Entity<CP14SkillStorageComponent> ent, ref GetVerbsEvent<Verb> args)
{
if (!_admin.HasAdminFlag(args.User, AdminFlags.Admin))
return;
if (_allSkills is null)
return;
var target = args.Target;
//Add Skill
foreach (var skill in _proto.EnumeratePrototypes<CP14SkillPrototype>())
foreach (var skill in _allSkills)
{
if (ent.Comp.LearnedSkills.Contains(skill))
continue;

View File

@@ -1,21 +1,21 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill.Specials;
namespace Content.Shared._CP14.Skill.Effects;
public sealed partial class AddAction : CP14SkillEffect
{
[DataField(required: true)]
public EntProtoId Action;
public override void AddSkill(EntityManager entManager, EntityUid target)
public override void AddSkill(IEntityManager entManager, EntityUid target)
{
var actionsSystem = entManager.System<SharedActionsSystem>();
actionsSystem.AddAction(target, Action);
}
public override void RemoveSkill(EntityManager entManager, EntityUid target)
public override void RemoveSkill(IEntityManager entManager, EntityUid target)
{
var actionsSystem = entManager.System<SharedActionsSystem>();
@@ -34,19 +34,13 @@ public sealed partial class AddAction : CP14SkillEffect
}
}
public override string? GetName(EntityManager entMagager, IPrototypeManager protoManager)
public override string? GetName(IEntityManager entMagager, IPrototypeManager protoManager)
{
if (!protoManager.TryIndex(Action, out var indexedAction))
return String.Empty;
return indexedAction.Name;
return !protoManager.TryIndex(Action, out var indexedAction) ? string.Empty : indexedAction.Name;
}
public override string? GetDescription(EntityManager entMagager, IPrototypeManager protoManager)
public override string? GetDescription(IEntityManager entMagager, IPrototypeManager protoManager)
{
if (!protoManager.TryIndex(Action, out var indexedAction))
return String.Empty;
return indexedAction.Description;
return !protoManager.TryIndex(Action, out var indexedAction) ? string.Empty : indexedAction.Description;
}
}

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
using System.Numerics;
using Content.Shared._CP14.Skill.Specials;
using Content.Shared._CP14.Skill.Effects;
using Content.Shared._CP14.Skill.Restrictions;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -37,12 +38,6 @@ public sealed partial class CP14SkillPrototype : IPrototype
[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>
@@ -61,4 +56,10 @@ public sealed partial class CP14SkillPrototype : IPrototype
/// </summary>
[DataField]
public CP14SkillEffect? Effect;
/// <summary>
/// Skill restriction. Limiters on learning. Any reason why a player cannot learn this skill.
/// </summary>
[DataField]
public List<CP14SkillRestriction> Restrictions = new();
}

View File

@@ -0,0 +1,13 @@
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill.Restrictions;
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract partial class CP14SkillRestriction
{
public abstract bool Check(IEntityManager entManager, EntityUid target);
public abstract string GetDescription(IEntityManager entManager, IPrototypeManager protoManager);
}

View File

@@ -0,0 +1,27 @@
using Content.Shared._CP14.Skill.Components;
using Content.Shared._CP14.Skill.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill.Restrictions;
public sealed partial class NeedPrerequisite : CP14SkillRestriction
{
[DataField(required: true)]
public ProtoId<CP14SkillPrototype> Prerequisite = new();
public override bool Check(IEntityManager entManager, EntityUid target)
{
if (!entManager.TryGetComponent<CP14SkillStorageComponent>(target, out var skillStorage))
return false;
var learned = skillStorage.LearnedSkills;
return learned.Contains(Prerequisite);
}
public override string GetDescription(IEntityManager entManager, IPrototypeManager protoManager)
{
var skillSystem = entManager.System<CP14SharedSkillSystem>();
return Loc.GetString("cp14-skill-req-prerequisite", ("name", skillSystem.GetSkillName(Prerequisite)));
}
}

View File

@@ -0,0 +1,26 @@
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Skill.Restrictions;
public sealed partial class SpeciesWhitelist : CP14SkillRestriction
{
[DataField(required: true)]
public ProtoId<SpeciesPrototype> Species = new();
public override bool Check(IEntityManager entManager, EntityUid target)
{
if (!entManager.TryGetComponent<HumanoidAppearanceComponent>(target, out var appearance))
return false;
return appearance.Species == Species;
}
public override string GetDescription(IEntityManager entManager, IPrototypeManager protoManager)
{
var species = protoManager.Index(Species);
return Loc.GetString("cp14-skill-req-species", ("name", Loc.GetString(species.Name)));
}
}

View File

@@ -1,17 +0,0 @@
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

@@ -0,0 +1,2 @@
cp14-skill-req-prerequisite = Skill "{$name}" must be learned.
cp14-skill-req-species = Available only to species "{$name}"

View File

@@ -0,0 +1,2 @@
cp14-skill-req-prerequisite = Навык "{$name}" должен быть изучен.
cp14-skill-req-species = Доступно только расе "{$name}"

View File

@@ -24,8 +24,8 @@
- !type:Polymorph
prototype: CP14Sheep
- type: CP14MagicEffectVerbalAspect
startSpeech: "Pellis dolorem..."
endSpeech: "non novit"
startSpeech: "Pascam et custodiam..."
endSpeech: "pecora tua"
- type: CP14MagicEffectCastingVisual
proto: CP14RuneSheepPolymorph
- type: EntityTargetAction

View File

@@ -1,13 +1,3 @@
- type: entity
id: CP14DummyActionSpellManaManipulation
name: Mana manipulation
description: You can manipulate mana by giving it to other objects or taking it from them.
categories: [ HideSpawnMenu ]
components:
- type: Sprite
sprite: _CP14/Actions/Spells/meta.rsi
state: mana
- type: entity
id: CP14ActionSpellManaGift
name: Mana transfer
@@ -54,6 +44,25 @@
castTime: 10
breakOnMove: true
- type: entity
id: CP14ActionSpellManaGiftElf
parent: CP14ActionSpellManaGift
name: Carefull mana transfer
description: You transfer mana at a tremendous rate without dealing damage to the target.
components:
- type: CP14MagicEffectManaCost
manaCost: 20
- type: CP14MagicEffect
effects:
- !type:CP14SpellSpawnEntityOnTarget
spawns:
- CP14ImpactEffectManaGift
- !type:CP14SpellApplyEntityEffect
effects:
- !type:CP14ManaChange
manaDelta: 20
safe: true
- type: entity
id: CP14RuneManaGift
parent: CP14BaseMagicRune

View File

@@ -1,12 +1,12 @@
- type: loadoutGroup
id: CP14SkillTree
name: cp14-loadout-skill-tree
minLimit: 0
minLimit: 2
maxLimit: 2
loadouts:
- CP14SkillTreePyrokinetic
- CP14SkillTreeHydrosophistry
- CP14SkillTreeMetamagic
- CP14SkillTreeHydrosophistry
- CP14SkillTreePyrokinetic
- CP14SkillTreeIllusion
- CP14SkillTreeHealing
- CP14SkillTreeAtlethic

View File

@@ -16,4 +16,21 @@
sprite: _CP14/Actions/Spells/physical.rsi
state: sprint
effect: !type:AddAction
action: CP14ActionSpellSprint
action: CP14ActionSpellSprint
- type: cp14Skill
id: CP14ActionSpellSprintGoblin
skillUiPosition: 2, 2
learnCost: 0
tree: Atlethic
icon:
sprite: _CP14/Actions/Spells/physical.rsi
state: sprint
effect: !type:ReplaceAction
oldAction: CP14ActionSpellSprint
newAction: CP14ActionSpellSprintGoblin
restrictions:
- !type:NeedPrerequisite
prerequisite: CP14ActionSpellSprint
- !type:SpeciesWhitelist
species: CP14Goblin

View File

@@ -27,6 +27,9 @@
state: beer_creation
effect: !type:AddAction
action: CP14ActionSpellBeerCreation
restrictions:
- !type:SpeciesWhitelist
species: CP14Dwarf
- type: cp14Skill
id: CP14ActionSpellIceShards

View File

@@ -20,16 +20,50 @@
action: CP14ActionSpellManaGift
- type: cp14Skill
id: CP14ActionSpellManaConsume
id: CP14ActionSpellManaGiftElf
skillUiPosition: 2, 2
learnCost: 0.5
tree: Metamagic
icon:
sprite: _CP14/Actions/Spells/meta.rsi
state: mana_gift
effect: !type:ReplaceAction
oldAction: CP14ActionSpellManaGift
newAction: CP14ActionSpellManaGiftElf
restrictions:
- !type:NeedPrerequisite
prerequisite: CP14ActionSpellManaGift
- !type:SpeciesWhitelist
species: CP14Elf
- type: cp14Skill
id: CP14ActionSpellManaConsume
skillUiPosition: 0, 4
learnCost: 0.5
tree: Metamagic
icon:
sprite: _CP14/Actions/Spells/meta.rsi
state: mana_consume
effect: !type:AddAction
action: CP14ActionSpellManaConsume
- type: cp14Skill
id: CP14ActionSpellManaConsumeElf
skillUiPosition: 2, 4
learnCost: 0.5
tree: Metamagic
icon:
sprite: _CP14/Actions/Spells/meta.rsi
state: mana_consume
effect: !type:ReplaceAction
oldAction: CP14ActionSpellManaConsume
newAction: CP14ActionSpellManaConsumeElf
restrictions:
- !type:NeedPrerequisite
prerequisite: CP14ActionSpellManaConsume
- !type:SpeciesWhitelist
species: CP14Elf
- type: cp14Skill
id: CP14ActionSpellMagicBallade
skillUiPosition: 0, 6

View File

@@ -10,10 +10,26 @@
- type: cp14Skill
id: CP14ActionSpellHellBallade
skillUiPosition: 0, 10
skillUiPosition: 0, 2
tree: Pyrokinetic
icon:
sprite: _CP14/Actions/Spells/fire.rsi
state: fire_music
effect: !type:AddAction
action: CP14ActionSpellHellBallade
action: CP14ActionSpellHellBallade
restrictions:
- !type:SpeciesWhitelist
species: CP14Tiefling
- type: cp14Skill
id: CP14ActionSpellTieflingInnerFire
skillUiPosition: 0, 4
tree: Pyrokinetic
icon:
sprite: _CP14/Actions/Spells/fire.rsi
state: tiefling_revenge
effect: !type:AddAction
action: CP14ActionSpellTieflingInnerFire
restrictions:
- !type:SpeciesWhitelist
species: CP14Tiefling