diff --git a/Content.Client/NPC/Systems/NpcFactionSpriteStateSetterSystem.cs b/Content.Client/NPC/Systems/NpcFactionSpriteStateSetterSystem.cs new file mode 100644 index 0000000000..1e57d917be --- /dev/null +++ b/Content.Client/NPC/Systems/NpcFactionSpriteStateSetterSystem.cs @@ -0,0 +1,28 @@ + +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Events; +using Robust.Client.GameObjects; +using Robust.Shared.Reflection; + +namespace Content.Client.NPC.Systems; +public sealed partial class NpcFactionSpriteStateSetterSystem : EntitySystem +{ + [Dependency] private readonly SpriteSystem _spriteSystem = default!; + [Dependency] private readonly EntityManager _entityManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnFactionAdded); + } + + private void OnFactionAdded(Entity entity, ref NpcFactionAddedEvent args) + { + if (!_entityManager.HasComponent(entity, typeof(NpcFactionSpriteStateSetterComponent))) + return; + + SpriteComponent spriteComponent = _entityManager.GetComponent(entity); + spriteComponent.LayerSetState(0, new Robust.Client.Graphics.RSI.StateId(args.FactionID)); + } +} diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs index 2ae20817e2..a5279ae857 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Server.NPC.Components; +using Content.Server.NPC.HTN; using Content.Shared.CombatMode; using Content.Shared.NPC; using Robust.Shared.Map; @@ -10,6 +11,7 @@ namespace Content.Server.NPC.Systems; public sealed partial class NPCCombatSystem { + [Dependency] private readonly IRobustRandom _rng = default!; private const float TargetMeleeLostRange = 14f; private void InitializeMelee() @@ -114,5 +116,8 @@ public sealed partial class NPCCombatSystem { _melee.AttemptLightAttack(uid, weaponUid, weapon, component.Target); } + + if (Comp(uid).Blackboard.TryGetValue("AttackDelayDeviation", out var dev, EntityManager)) + weapon.NextAttack += TimeSpan.FromSeconds(_rng.NextFloat(-dev, dev)); } } diff --git a/Content.Shared/NPC/Components/NpcFactionSelectorComponent.cs b/Content.Shared/NPC/Components/NpcFactionSelectorComponent.cs new file mode 100644 index 0000000000..fcc40e9b60 --- /dev/null +++ b/Content.Shared/NPC/Components/NpcFactionSelectorComponent.cs @@ -0,0 +1,11 @@ +using Content.Shared.NPC.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.NPC.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(NpcFactionSelectorSystem))] +public sealed partial class NpcFactionSelectorComponent : Component +{ + [DataField] + public List SelectableFactions = new(); +} diff --git a/Content.Shared/NPC/Components/NpcFactionSpriteStateSetterComponent.cs b/Content.Shared/NPC/Components/NpcFactionSpriteStateSetterComponent.cs new file mode 100644 index 0000000000..ec5d66ff03 --- /dev/null +++ b/Content.Shared/NPC/Components/NpcFactionSpriteStateSetterComponent.cs @@ -0,0 +1,7 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.NPC.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class NpcFactionSpriteStateSetterComponent : Component {} + diff --git a/Content.Shared/NPC/Events/NpcFactionChangedEvent.cs b/Content.Shared/NPC/Events/NpcFactionChangedEvent.cs new file mode 100644 index 0000000000..dc43017af6 --- /dev/null +++ b/Content.Shared/NPC/Events/NpcFactionChangedEvent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.NPC.Events; + +/// +/// Raised from client to server to notify a faction was added to an NPC. +/// +[Serializable, NetSerializable] +public sealed class NpcFactionAddedEvent : EntityEventArgs +{ + public string FactionID; + + public NpcFactionAddedEvent(string factionId) => FactionID = factionId; +} + +/// +/// Raised from client to server to notify a faction was removed from an NPC. +/// +[Serializable, NetSerializable] +public sealed class NpcFactionRemovedEvent : EntityEventArgs +{ + public string FactionID; + + public NpcFactionRemovedEvent(string factionId) => FactionID = factionId; +} diff --git a/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs b/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs index 1dcdd751c8..5a7b403814 100644 --- a/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs +++ b/Content.Shared/NPC/Prototypes/NpcFactionPrototype.cs @@ -24,6 +24,9 @@ public sealed partial class NpcFactionPrototype : IPrototype /// public record struct FactionData { + [ViewVariables] + public bool IsHostileToSelf; + [ViewVariables] public HashSet> Friendly; diff --git a/Content.Shared/NPC/Systems/NpcFactionSelectorSystem.cs b/Content.Shared/NPC/Systems/NpcFactionSelectorSystem.cs new file mode 100644 index 0000000000..c3fef47da4 --- /dev/null +++ b/Content.Shared/NPC/Systems/NpcFactionSelectorSystem.cs @@ -0,0 +1,60 @@ +using Content.Shared.Database; +using Content.Shared.NPC.Components; +using Content.Shared.NPC.Prototypes; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Robust.Shared.Prototypes; +using System.Linq; + +namespace Content.Shared.NPC.Systems; +public sealed partial class NpcFactionSelectorSystem : EntitySystem +{ + + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly NpcFactionSystem _factionSystem = default!; + [Dependency] private readonly EntityManager _entityManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnGetVerb); + } + + private void OnGetVerb(Entity entity, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null) + return; + + NpcFactionSelectorComponent factionSelectorComponent = _entityManager.GetComponent(entity); + + if (factionSelectorComponent.SelectableFactions.Count < 2) + return; + + foreach (var type in factionSelectorComponent.SelectableFactions) + { + var proto = _prototype.Index(type); + + var v = new Verb + { + Priority = 1, + Category = VerbCategory.SelectFaction, + Text = proto.ID, + Impact = LogImpact.Medium, + DoContactInteraction = true, + Act = () => + { + _popup.PopupPredicted(Loc.GetString("npcfaction-component-faction-set", ("faction", proto.ID)), entity.Owner, null); + foreach (var type in factionSelectorComponent.SelectableFactions) + { + _factionSystem.RemoveFaction(entity.Owner, type); + } + + _factionSystem.AddFaction(entity.Owner, proto.ID); + } + }; + args.Verbs.Add(v); + } + } +} diff --git a/Content.Shared/NPC/Systems/NpcFactionSystem.cs b/Content.Shared/NPC/Systems/NpcFactionSystem.cs index 448c7eeaec..62de929864 100644 --- a/Content.Shared/NPC/Systems/NpcFactionSystem.cs +++ b/Content.Shared/NPC/Systems/NpcFactionSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.NPC.Components; +using Content.Shared.NPC.Events; using Content.Shared.NPC.Prototypes; using Robust.Shared.Prototypes; using System.Collections.Frozen; @@ -106,6 +107,8 @@ public sealed partial class NpcFactionSystem : EntitySystem if (!ent.Comp.Factions.Add(faction)) return; + RaiseLocalEvent(ent.Owner, new NpcFactionAddedEvent(faction)); + if (dirty) RefreshFactions((ent, ent.Comp)); } @@ -125,6 +128,8 @@ public sealed partial class NpcFactionSystem : EntitySystem continue; } + RaiseLocalEvent(ent.Owner, new NpcFactionAddedEvent(faction)); + ent.Comp.Factions.Add(faction); } @@ -149,6 +154,8 @@ public sealed partial class NpcFactionSystem : EntitySystem if (!ent.Comp.Factions.Remove(faction)) return; + RaiseLocalEvent(ent.Owner, new NpcFactionRemovedEvent(faction)); + if (dirty) RefreshFactions((ent, ent.Comp)); } @@ -217,7 +224,12 @@ public sealed partial class NpcFactionSystem : EntitySystem if (!Resolve(ent, ref ent.Comp, false) || !Resolve(other, ref other.Comp, false)) return false; - return ent.Comp.Factions.Overlaps(other.Comp.Factions) || ent.Comp.FriendlyFactions.Overlaps(other.Comp.Factions); + var intersect = ent.Comp.Factions.Intersect(other.Comp.Factions); // factions which have both ent and other as members + foreach (var faction in intersect) + if (_factions[faction].IsHostileToSelf) + return false; + + return intersect.Count() > 0 || ent.Comp.FriendlyFactions.Overlaps(other.Comp.Factions); } public bool IsFactionFriendly(string target, string with) @@ -301,8 +313,9 @@ public sealed partial class NpcFactionSystem : EntitySystem { _factions = _proto.EnumeratePrototypes().ToFrozenDictionary( faction => faction.ID, - faction => new FactionData + faction => new FactionData { + IsHostileToSelf = faction.Hostile.Contains(faction.ID), Friendly = faction.Friendly.ToHashSet(), Hostile = faction.Hostile.ToHashSet() }); diff --git a/Content.Shared/Verbs/VerbCategory.cs b/Content.Shared/Verbs/VerbCategory.cs index 38ec881a7c..be35479a36 100644 --- a/Content.Shared/Verbs/VerbCategory.cs +++ b/Content.Shared/Verbs/VerbCategory.cs @@ -100,6 +100,8 @@ namespace Content.Shared.Verbs public static readonly VerbCategory SelectType = new("verb-categories-select-type"); + public static readonly VerbCategory SelectFaction = new("verb-categories-select-faction"); + public static readonly VerbCategory PowerLevel = new("verb-categories-power-level"); public static readonly VerbCategory Interaction = new("verb-categories-interaction"); diff --git a/Resources/Locale/en-US/interaction/interaction-popup-component.ftl b/Resources/Locale/en-US/interaction/interaction-popup-component.ftl index 40d9975a46..aced542aaf 100644 --- a/Resources/Locale/en-US/interaction/interaction-popup-component.ftl +++ b/Resources/Locale/en-US/interaction/interaction-popup-component.ftl @@ -72,6 +72,7 @@ petting-success-syndicate-cyborg = You pet {THE($target)} on {POSS-ADJ($target)} petting-success-derelict-cyborg = You pet {THE($target)} on {POSS-ADJ($target)} rusty metal head. petting-success-recycler = You pet {THE($target)} on {POSS-ADJ($target)} mildly threatening steel exterior. petting-success-station-ai = You pet {THE($target)} on {POSS-ADJ($target)} cold, square screen. +petting-success-gladiabot = You pet {THE($target)} on {POSS-ADJ($target)} vicious cardboard head. petting-failure-honkbot = You reach out to pet {THE($target)}, but {SUBJECT($target)} honks in refusal! petting-failure-cleanbot = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} busy mopping! @@ -87,6 +88,7 @@ petting-failure-service-cyborg = You reach out to pet {THE($target)}, but {SUBJE petting-failure-syndicate-cyborg = You reach out to pet {THE($target)}, but {POSS-ADJ($target)} treacherous affiliation makes you reconsider. petting-failure-derelict-cyborg = You reach out to pet {THE($target)}, but {POSS-ADJ($target)} rusty and jagged exterior makes you reconsider. petting-failure-station-ai = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BASIC($target, "zap", "zaps")} your hand away. +petting-failure-gladiabot = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} only wants to fight! petting-success-station-ai-others = { CAPITALIZE(THE($user)) } pets {THE($target)} on {POSS-ADJ($target)} cold, square screen. diff --git a/Resources/Locale/en-US/npc/factions.ftl b/Resources/Locale/en-US/npc/factions.ftl new file mode 100644 index 0000000000..8aaa2b6e8f --- /dev/null +++ b/Resources/Locale/en-US/npc/factions.ftl @@ -0,0 +1 @@ +npcfaction-component-faction-set = Faction set to: {$faction} diff --git a/Resources/Locale/en-US/verbs/verb-system.ftl b/Resources/Locale/en-US/verbs/verb-system.ftl index c99f9987cb..d579f045c5 100644 --- a/Resources/Locale/en-US/verbs/verb-system.ftl +++ b/Resources/Locale/en-US/verbs/verb-system.ftl @@ -26,6 +26,7 @@ verb-categories-set-sensor = Sensor verb-categories-timer = Set Delay verb-categories-lever = Lever verb-categories-select-type = Select Type +verb-categories-select-faction = Select Faction verb-categories-fax = Set Destination verb-categories-power-level = Power Level verb-categories-interaction = Interact diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/gladiabot.yml b/Resources/Prototypes/Entities/Mobs/NPCs/gladiabot.yml new file mode 100644 index 0000000000..78faa6f6cb --- /dev/null +++ b/Resources/Prototypes/Entities/Mobs/NPCs/gladiabot.yml @@ -0,0 +1,90 @@ +- type: entity + parent: MobSiliconBase + id: MobGladiaBot + name: gladiabot + description: For glory! + components: + - type: Sprite + sprite: Mobs/Silicon/Bots/gladiabot.rsi + state: GladiabotFFA + - type: Inventory + templateId: gladiabot + - type: InventorySlots + - type: UserInterface + interfaces: + enum.StrippingUiKey.Key: + type: StrippableBoundUserInterface + - type: Construction + graph: GladiaBot + node: bot + - type: Stripping + - type: Strippable + - type: SentienceTarget + flavorKind: station-event-random-sentience-flavor-mechanical + - type: UseDelay + delay: 1 + - type: NpcFactionMember + factions: + - GladiabotFFA + - type: NpcFactionSelector + selectableFactions: + - GladiabotFFA + - GladiabotRed + - GladiabotBlue + - GladiabotGreen + - GladiabotYellow + - type: NpcFactionSpriteStateSetter + - type: CombatMode + - type: MeleeWeapon + altDisarm: false + soundHit: + path: /Audio/Weapons/bladeslice.ogg + angle: 0 + animation: WeaponArcPunch + damage: + types: + Slash: 3 + - type: MobThresholds + thresholds: + 0: Alive + 40: Dead + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 30 + behaviors: + - !type:TriggerBehavior + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:SpawnEntitiesBehavior + spawn: + ProximitySensor: + min: 1 + max: 1 + - !type:SpawnEntitiesBehavior + spawn: + MaterialCloth1: + min: 1 + max: 1 + - !type:EmptyContainersBehaviour + containers: + - head + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: MovementSpeedModifier + baseWalkSpeed: 2 + baseSprintSpeed: 3 + - type: HTN + rootTask: + task: GladiabotCompound + blackboard: + AttackDelayDeviation: !type:Single + 0.3 + - type: InteractionPopup + interactSuccessString: petting-success-gladiabot + interactFailureString: petting-failure-gladiabot + interactSuccessSound: + path: /Audio/Ambience/Objects/periodic_beep.ogg diff --git a/Resources/Prototypes/InventoryTemplates/gladiabot_inventory_template.yml b/Resources/Prototypes/InventoryTemplates/gladiabot_inventory_template.yml new file mode 100644 index 0000000000..c7f0e44fe3 --- /dev/null +++ b/Resources/Prototypes/InventoryTemplates/gladiabot_inventory_template.yml @@ -0,0 +1,10 @@ +- type: inventoryTemplate + id: gladiabot + slots: + - name: head + slotTexture: head + slotFlags: HEAD + uiWindowPos: 0,1 + strippingWindowPos: 0,0 + displayName: Head + offset: 0.1, -0.15 diff --git a/Resources/Prototypes/NPCs/gladiabot.yml b/Resources/Prototypes/NPCs/gladiabot.yml new file mode 100644 index 0000000000..5e7170ee27 --- /dev/null +++ b/Resources/Prototypes/NPCs/gladiabot.yml @@ -0,0 +1,12 @@ +- type: htnCompound + id: GladiabotCompound + branches: + - tasks: + - !type:HTNPrimitiveTask + operator: !type:UtilityOperator + proto: NearbyMeleeTargets + - !type:HTNCompoundTask + task: MeleeAttackTargetCompound + - tasks: + - !type:HTNCompoundTask + task: IdleCompound diff --git a/Resources/Prototypes/Recipes/Crafting/Graphs/bots/gladiabot.yml b/Resources/Prototypes/Recipes/Crafting/Graphs/bots/gladiabot.yml new file mode 100644 index 0000000000..ddcfceaef4 --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/Graphs/bots/gladiabot.yml @@ -0,0 +1,25 @@ +- type: constructionGraph + id: GladiaBot + start: start + graph: + - node: start + edges: + - to: bot + steps: + - material: Cardboard + amount: 1 + doAfter: 2 + - tag: ProximitySensor + icon: + sprite: Objects/Misc/proximity_sensor.rsi + state: icon + name: proximity sensor + doAfter: 2 + - tag: Shiv + icon: + sprite: Objects/Weapons/Melee/shiv.rsi + state: icon + name: shiv + doAfter: 2 + - node: bot + entity: MobGladiaBot diff --git a/Resources/Prototypes/Recipes/Crafting/bots.yml b/Resources/Prototypes/Recipes/Crafting/bots.yml index 3031f4a780..8f67905b85 100644 --- a/Resources/Prototypes/Recipes/Crafting/bots.yml +++ b/Resources/Prototypes/Recipes/Crafting/bots.yml @@ -75,3 +75,16 @@ icon: sprite: Mobs/Silicon/Bots/supplybot.rsi state: supplybot + +- type: construction + name: gladiabot + id: gladiabot + graph: GladiaBot + startNode: start + targetNode: bot + category: construction-category-utilities + objectType: Item + description: This bot fights for honour and glory! + icon: + sprite: Mobs/Silicon/Bots/gladiabot.rsi + state: GladiabotFFA diff --git a/Resources/Prototypes/ai_factions.yml b/Resources/Prototypes/ai_factions.yml index c0f7c7da6a..4cbfc73f8e 100644 --- a/Resources/Prototypes/ai_factions.yml +++ b/Resources/Prototypes/ai_factions.yml @@ -107,3 +107,44 @@ - type: npcFaction id: AnimalFriend + +- type: npcFaction + id: GladiabotFFA + hostile: + - GladiabotFFA + - GladiabotRed + - GladiabotGreen + - GladiabotBlue + - GladiabotYellow + +- type: npcFaction + id: GladiabotRed + hostile: + - GladiabotFFA + - GladiabotGreen + - GladiabotBlue + - GladiabotYellow + +- type: npcFaction + id: GladiabotGreen + hostile: + - GladiabotFFA + - GladiabotRed + - GladiabotBlue + - GladiabotYellow + +- type: npcFaction + id: GladiabotBlue + hostile: + - GladiabotFFA + - GladiabotRed + - GladiabotGreen + - GladiabotYellow + +- type: npcFaction + id: GladiabotYellow + hostile: + - GladiabotFFA + - GladiabotRed + - GladiabotGreen + - GladiabotBlue diff --git a/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotBlue.png b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotBlue.png new file mode 100644 index 0000000000..800edcb860 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotBlue.png differ diff --git a/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotFFA.png b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotFFA.png new file mode 100644 index 0000000000..e8994abca1 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotFFA.png differ diff --git a/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotGreen.png b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotGreen.png new file mode 100644 index 0000000000..3e82302e6c Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotGreen.png differ diff --git a/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotRed.png b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotRed.png new file mode 100644 index 0000000000..68d0e24929 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotRed.png differ diff --git a/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotYellow.png b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotYellow.png new file mode 100644 index 0000000000..abdef3b926 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/GladiabotYellow.png differ diff --git a/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/meta.json b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/meta.json new file mode 100644 index 0000000000..d3ac6b30cf --- /dev/null +++ b/Resources/Textures/Mobs/Silicon/Bots/gladiabot.rsi/meta.json @@ -0,0 +1,56 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Tim Falken", + "states": [ + { + "name": "GladiabotFFA", + "delays": [ + [ + 0.5, + 0.2 + ] + ] + }, + { + "name": "GladiabotRed", + "delays": [ + [ + 0.5, + 0.2 + ] + ] + }, + { + "name": "GladiabotGreen", + "delays": [ + [ + 0.5, + 0.2 + ] + ] + }, + { + "name": "GladiabotBlue", + "delays": [ + [ + 0.5, + 0.2 + ] + ] + }, + { + "name": "GladiabotYellow", + "delays": [ + [ + 0.5, + 0.2 + ] + ] + } + ] +}