diff --git a/Content.Server/_CorvaxNext/Speech/Components/ParrotSpeechComponent.cs b/Content.Server/_CorvaxNext/Speech/Components/ParrotSpeechComponent.cs new file mode 100644 index 00000000000..1ad09fe7067 --- /dev/null +++ b/Content.Server/_CorvaxNext/Speech/Components/ParrotSpeechComponent.cs @@ -0,0 +1,40 @@ +using Content.Server.Speech.EntitySystems; +using Content.Shared.Whitelist; + +namespace Content.Server.Speech.Components; + +[RegisterComponent] +[Access(typeof(ParrotSpeechSystem))] +public sealed partial class ParrotSpeechComponent : Component +{ + /// + /// The maximum number of words the parrot can learn per phrase. + /// Phrases are 1 to MaxPhraseLength words in length. + /// + [DataField] + public int MaximumPhraseLength = 7; + + [DataField] + public int MaximumPhraseCount = 20; + + [DataField] + public int MinimumWait = 60; // 1 minutes + + [DataField] + public int MaximumWait = 120; // 2 minutes + + /// + /// The probability that a parrot will learn from something an overheard phrase. + /// + [DataField] + public float LearnChance = 0.2f; + + [DataField] + public EntityWhitelist Blacklist { get; private set; } = new(); + + [DataField] + public TimeSpan? NextUtterance; + + [DataField(readOnly: true)] + public List LearnedPhrases = new(); +} diff --git a/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs b/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs new file mode 100644 index 00000000000..ddfdfe5cee1 --- /dev/null +++ b/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs @@ -0,0 +1,85 @@ +using System.Linq; +using Content.Server.Chat.Systems; +using Content.Server.Speech.Components; +using Content.Shared.Mind.Components; +using Content.Shared.Whitelist; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Speech.EntitySystems; + +public sealed class ParrotSpeechSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly ChatSystem _chat = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnListen); + SubscribeLocalEvent(CanListen); + } + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + if (component.LearnedPhrases.Count == 0) + // This parrot has not learned any phrases, so can't say anything interesting. + continue; + if (TryComp(uid, out var mind) && mind.HasMind) + // Pause parrot speech when someone is controlling the parrot. + continue; + if (_timing.CurTime < component.NextUtterance) + continue; + + if (component.NextUtterance != null) + { + _chat.TrySendInGameICMessage( + uid, + _random.Pick(component.LearnedPhrases), + InGameICChatType.Speak, + hideChat: true, // Don't spam the chat with randomly generated messages + hideLog: true, // TODO: Don't spam admin logs either. + // If a parrot learns something inappropriate, admins can search for + // the player that said the inappropriate thing. + checkRadioPrefix: false); + } + + component.NextUtterance = _timing.CurTime + TimeSpan.FromSeconds(_random.Next(component.MinimumWait, component.MaximumWait)); + } + } + + private void OnListen(EntityUid uid, ParrotSpeechComponent component, ref ListenEvent args) + { + if (_random.Prob(component.LearnChance)) + { + // Very approximate word splitting. But that's okay: parrots aren't smart enough to + // split words correctly. + var words = args.Message.Split(" ", StringSplitOptions.RemoveEmptyEntries); + // Prefer longer phrases + var phraseLength = 1 + (int) (Math.Sqrt(_random.NextDouble()) * component.MaximumPhraseLength); + + var startIndex = _random.Next(0, Math.Max(0, words.Length - phraseLength + 1)); + + var phrase = string.Join(" ", words.Skip(startIndex).Take(phraseLength)).ToLower(); + + while (component.LearnedPhrases.Count >= component.MaximumPhraseCount) + { + _random.PickAndTake(component.LearnedPhrases); + } + + component.LearnedPhrases.Add(phrase); + } + } + + private void CanListen(EntityUid uid, ParrotSpeechComponent component, ref ListenAttemptEvent args) + { + if (_whitelistSystem.IsBlacklistPass(component.Blacklist, args.Source)) + args.Cancel(); + } +} diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/mobs/npcs/animals.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/mobs/npcs/animals.ftl new file mode 100644 index 00000000000..119ace125c5 --- /dev/null +++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/mobs/npcs/animals.ftl @@ -0,0 +1,3 @@ +ent-MobParrotSmart = { ent-MobParrot } + .desc = Проникает в ваши владения, шпионит за вами и при этом остаётся классным питомцем. Умеет говорить. + .suffix = Умный diff --git a/Resources/Prototypes/Catalog/Fills/Crates/npc.yml b/Resources/Prototypes/Catalog/Fills/Crates/npc.yml index 10c715bb99e..dd9fd1d16e0 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/npc.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/npc.yml @@ -181,7 +181,7 @@ components: - type: StorageFill contents: - - id: MobParrot + - id: MobParrotSmart # Corvax-Next-Parrot: replaced MobParrot amount: 3 - type: entity diff --git a/Resources/Prototypes/Corvax/Entities/Markers/Spawners/Random/animals.yml b/Resources/Prototypes/Corvax/Entities/Markers/Spawners/Random/animals.yml index 23d44481582..4d18914fa1a 100644 --- a/Resources/Prototypes/Corvax/Entities/Markers/Spawners/Random/animals.yml +++ b/Resources/Prototypes/Corvax/Entities/Markers/Spawners/Random/animals.yml @@ -32,7 +32,7 @@ - MobLizard - MobSlug - MobFrog - - MobParrot + - MobParrotSmart # Corvax-Next-Parrot: replaced MobParrot - MobPenguin - MobSnake - MobPossum diff --git a/Resources/Prototypes/Entities/Markers/Spawners/mobs.yml b/Resources/Prototypes/Entities/Markers/Spawners/mobs.yml index 723cd1148b1..2122bca7b33 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/mobs.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/mobs.yml @@ -721,7 +721,7 @@ sprite: Mobs/Animals/parrot.rsi - type: ConditionalSpawner prototypes: - - MobParrot + - MobParrotSmart # Corvax-Next-Parrot: replaced MobParrot - type: entity name: Butterfly Spawner diff --git a/Resources/Prototypes/XenoArch/Effects/normal_effects.yml b/Resources/Prototypes/XenoArch/Effects/normal_effects.yml index f2723e5369e..46603de45f5 100644 --- a/Resources/Prototypes/XenoArch/Effects/normal_effects.yml +++ b/Resources/Prototypes/XenoArch/Effects/normal_effects.yml @@ -392,7 +392,7 @@ prob: 0.03 - id: MobMouse orGroup: fauna - - id: MobParrot + - id: MobParrotSmart # Corvax-Next-Parrot: replaced MobParrot orGroup: fauna maxAmount: 1 - id: MobPenguin diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml new file mode 100644 index 00000000000..9ab8d6a0420 --- /dev/null +++ b/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml @@ -0,0 +1,88 @@ +- type: entity + name: parrot + parent: [ SimpleMobBase, FlyingMobBase ] + id: MobParrotSmart + description: Infiltrates your domain, spies on you, and somehow still a cool pet. Actually can talk. + suffix: Smart + components: + - type: MovementSpeedModifier + baseWalkSpeed : 2.5 + baseSprintSpeed : 6 + - type: Sprite + drawdepth: Mobs + layers: + - map: ["enum.DamageStateVisualLayers.Base", "movement"] + state: parrot-moving # until upstream + sprite: _CorvaxNext/Mobs/Animals/parrot.rsi + - type: SpriteMovement + movementLayers: + movement: + state: parrot-moving + noMovementLayers: + movement: + state: parrot-moving # until upstream + - type: Item + sprite: _CorvaxNext/Mobs/Animals/parrot.rsi + size: Normal + inhandVisuals: + left: + - state: inhand-left + right: + - state: inhand-right + - type: Clothing + quickEquip: false + sprite: _CorvaxNext/Mobs/Animals/parrot.rsi + slots: + - HEAD + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.25 + density: 10 + mask: + - FlyingMobMask + layer: + - FlyingMobLayer + - type: Appearance + - type: DamageStateVisuals + states: + Alive: + Base: parrot + Critical: + Base: dead + Dead: + Base: dead + - type: Butcherable + spawned: + - id: FoodMeat + amount: 1 + - type: Speech + speechSounds: Parrot + speechVerb: Parrot + - type: Vocal + sounds: + Unsexed: Parrot + - type: CanEscapeInventory + - type: ParrotAccent + - type: Strippable + - type: InteractionPopup + successChance: 0.6 + interactSuccessString: petting-success-bird + interactFailureString: petting-failure-generic + interactSuccessSpawn: EffectHearts + interactSuccessSound: + path: /Audio/Animals/parrot_raught.ogg + - type: Bloodstream + bloodMaxVolume: 50 + - type: ActiveListener + range: 5 + - type: ParrotSpeech + blacklist: + components: + - ParrotSpeech # Stop parrots repeating their own speech + - SurveillanceCamera + - SurveillanceCameraMonitor + - RadioSpeaker + - VendingMachine \ No newline at end of file diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/dead.png b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/dead.png new file mode 100644 index 00000000000..8a87b42a3be Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/dead.png differ diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/equipped-HELMET.png b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/equipped-HELMET.png new file mode 100644 index 00000000000..f44994cc2f1 Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/equipped-HELMET.png differ diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/icon.png b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/icon.png new file mode 100644 index 00000000000..10459e3c3ce Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/icon.png differ diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/inhand-left.png b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/inhand-left.png new file mode 100644 index 00000000000..1203ff93458 Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/inhand-left.png differ diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/inhand-right.png b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/inhand-right.png new file mode 100644 index 00000000000..5385fd47db0 Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/inhand-right.png differ diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/meta.json b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/meta.json new file mode 100644 index 00000000000..e0d80415b48 --- /dev/null +++ b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/meta.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation/commit/53d1f1477d22a11a99c6c6924977cd431075761b", + "states": [ + { + "name": "parrot-moving", + "directions": 4, + "delays": [ + [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] + ] + }, + { + "name": "parrot", + "directions": 4 + }, + { + "name": "dead" + }, + { + "name": "icon" + }, + { + "name": "equipped-HELMET", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/parrot-moving.png b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/parrot-moving.png new file mode 100644 index 00000000000..fd9079fd668 Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/parrot-moving.png differ diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/parrot.png b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/parrot.png new file mode 100644 index 00000000000..fd78a533ee9 Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/parrot.png differ