From b5af1591c4a9a944f72f1ca94afd4b8691110220 Mon Sep 17 00:00:00 2001 From: Vonsant Date: Wed, 25 Dec 2024 00:51:58 +0300 Subject: [PATCH 1/5] YaRRRR --- .../Components/ParrotSpeechComponent.cs | 40 ++++++++ .../EntitySystems/ParrotSpeechSystem.cs | 85 +++++++++++++++++ .../entities/mobs/npcs/animals.ftl | 3 + .../Prototypes/Catalog/Fills/Crates/npc.yml | 2 +- .../Markers/Spawners/Random/animals.yml | 2 +- .../Entities/Markers/Spawners/mobs.yml | 2 +- .../XenoArch/Effects/normal_effects.yml | 2 +- .../Entities/Mobs/NPCs/animals.yml | 88 ++++++++++++++++++ .../Mobs/Animals/parrot.rsi/dead.png | Bin 0 -> 647 bytes .../Animals/parrot.rsi/equipped-HELMET.png | Bin 0 -> 412 bytes .../Mobs/Animals/parrot.rsi/icon.png | Bin 0 -> 593 bytes .../Mobs/Animals/parrot.rsi/inhand-left.png | Bin 0 -> 657 bytes .../Mobs/Animals/parrot.rsi/inhand-right.png | Bin 0 -> 662 bytes .../Mobs/Animals/parrot.rsi/meta.json | 43 +++++++++ .../Mobs/Animals/parrot.rsi/parrot-moving.png | Bin 0 -> 16667 bytes .../Mobs/Animals/parrot.rsi/parrot.png | Bin 0 -> 1264 bytes 16 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 Content.Server/_CorvaxNext/Speech/Components/ParrotSpeechComponent.cs create mode 100644 Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs create mode 100644 Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/mobs/npcs/animals.ftl create mode 100644 Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/dead.png create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/equipped-HELMET.png create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/icon.png create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/inhand-left.png create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/inhand-right.png create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/meta.json create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/parrot-moving.png create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/parrot.png 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 0000000000000000000000000000000000000000..8a87b42a3bee00e95f9eb741a0bfe42e96ea4e62 GIT binary patch literal 647 zcmV;20(kw2P)ML+0p=@i*K#iLq=9R7tAWho(15=fsIT*ZUJD>_lA4HIF-sl8gfqK&OJlb z>~Zjok1f~XQim2xFH^FEVZ>Q9}MACccI$(GUHLK&T(mBY

nrmghHUb@6`qG7k*`<1Kqtw2;jn8VMV9PvXuT@4I8wgB}t>{^E{ky0MZXFt2 h_pfSTU|{fn^9`Jc)H?=Fi!lHI002ovPDHLkV1i#nIY|Hj literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f44994cc2f111b247566fbf93a1f0d3d5df708f4 GIT binary patch literal 412 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%z!#rIaLn`LH zy|vKqu!9WShxIHQpBxeLxYagc&7%AZE#fQgDWCZ!ndYYUG1yu8)%o0}?K7A*vTV$Z z`m5;0s&Vag8PC4?mX}}ufBjeAw7j9)@`cI$sqcTCEPY|xaQ&B#L$=bU50_hCCU|5k z`LR1M|0N!)e$T#6_UVl}>&hC+7fbPP_dl?rtm^DdJ%@^7)}B{sf7Ty5dFm&7 z%{FTX7X5|sf7RjRNa}IvylElQsA1f`iw6q&OQ4d9$-}?3d3J|Z2N!^<#0t*jacz<0JA&v|g zEj&Jm2p|p^f&dB+ssRh500^-P3LR3XV3m}pEEUcm8ITMa0S+WcNJv{-TL2AF026!w z6@gApP5>5x0TX-y5^EF{HxLjq01|2%YSa*3$=gp%TL~#{00001bW%=J06^y0W&i*I zKS@MER5*>*ky}&3FcgGOnl_hG(xz0vR&FVRP@n?Zs#W>_|H{aylXj?2zIgJG?Cdvt zb|wISH3%&HBe11)!?zuad=r>hj@=*}M46GmC~_JB$i{}oTGt{7=gc*<%Qz<{VAGC= zjTPAIP}}gkgbKg5!WWcu4ZtS8IF}59;9OEjVjBPu8H%31qbq&%-4mf->p;T36d0nx z@On6i01Lk#ngIZDB6W#~-{_+OUnX(Qhh!9wb;N{oKG6Z=xSP}g7{yaLQ|X+~Z`Djr z<59zh1zLLYZl&gGwU*uzEt&wJjBeC+m+rP|L$l^*eop47bafrCt8I>l^l)r-0UjO;Rg_Py{_{&wl~qx_wgF}Jrh)gWYy(bTz^T2& f2`c!-{=f49C|eoyk3tL*00000NkvXXu0mjfz&7mH literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1203ff9345803d2a937f88c0026035950d2d5e05 GIT binary patch literal 657 zcmV;C0&e|@P)u7Zn$;2^sA z8vGA*6lu#78?A|>%AMJ-ELLux^EL$=AX%Kdkq zUZKA)?B0}un)@Jl?t;2a_}B=7_x&LFIDo1 ztU>#&lXvr9Qk9%&p#V=u(42$TOK6x4v-*C|;SFS0XV$$;CbMg1B>(^b002I>lYP5{ubL;Jxec&PR&+;Zng0}IiZfYl&#>*` z{q(Zw^z8MluZ3AInHJ_-Xj<5&(=(-odjni2pg+uYOoIBu+%s01-L#*cq}k0kS5nJ8 zF@mrzt-4$l#sgKyRn2Q&?xNh&urBNW2ujlAe)f(80000000000;4i{F2~k$@Kcnc5 rp4;eV%bMN*0C*AUg9ZQqz~6ZQ_f9!5rd+Pf00000NkvXXu0mjfi&Qff literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5385fd47db0b936293c7e7fb2024fbc177673dd9 GIT binary patch literal 662 zcmV;H0%`q;P)u7Zn$;2^sA z8vGA*6lu#78?A|>%AMJ-ELLux^EL$=AX%Kdkq zUZKA)?B0}un)@Jl?t;2a_}B=7_x&LFIDo1 ztU>#&lXvr9Qk9%&p#V=u(42$TOK6x4v-*C|;SFS0=y83_^!JFo$jU1(aFDcaNp2BY1!LI{ZsIj z(g)*wottj$us_Om8ak{e8TU3`6np${+;p4$Cn!nX^X~OD0000000000;J?JYH(*{C w_1-|D>)t@#IdbG1k00001E-LdmFo(_Vt^fc407*qoM6N<$g1MkF=l}o! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fd9079fd668d12dc3a1d92fef2e2a6a71ed478bc GIT binary patch literal 16667 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 ag7O!_w^3=EHkxbz0000 Date: Wed, 25 Dec 2024 01:09:53 +0300 Subject: [PATCH 2/5] addTTS --- Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml index 9ab8d6a0420..6915a82d5af 100644 --- a/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml @@ -78,6 +78,8 @@ bloodMaxVolume: 50 - type: ActiveListener range: 5 + - type: TTS + voice: Livsy - type: ParrotSpeech blacklist: components: From 6761b2515d5c68833510a06a53f3f628c86eea89 Mon Sep 17 00:00:00 2001 From: Kill_Me_I_Noobs <118206719+Vonsant@users.noreply.github.com> Date: Wed, 25 Dec 2024 01:16:08 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=83=D0=B3=D0=B0=D0=B9?= =?UTF-8?q?=20=D0=BE=D0=BA=D0=B0=D0=B7=D0=B0=D0=BB=D1=81=D1=8F=20=D1=83?= =?UTF-8?q?=D0=BC=D0=BD=D0=B5=D0=B5=20=D0=BC=D0=B5=D0=BD=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml index 6915a82d5af..8ef42e980e0 100644 --- a/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml @@ -87,4 +87,4 @@ - SurveillanceCamera - SurveillanceCameraMonitor - RadioSpeaker - - VendingMachine \ No newline at end of file + - VendingMachine From a28b78cd8615f7acad64f29186980e522a3fa5f0 Mon Sep 17 00:00:00 2001 From: Kill_Me_I_Noobs <118206719+Vonsant@users.noreply.github.com> Date: Wed, 25 Dec 2024 01:29:24 +0300 Subject: [PATCH 4/5] Update Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs Co-authored-by: FN <37689533+FireNameFN@users.noreply.github.com> --- .../_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs b/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs index ddfdfe5cee1..940a9a72ed5 100644 --- a/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs +++ b/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs @@ -37,7 +37,7 @@ public override void Update(float frameTime) if (_timing.CurTime < component.NextUtterance) continue; - if (component.NextUtterance != null) + if (component.NextUtterance is not null) { _chat.TrySendInGameICMessage( uid, From 393fef045cb8007b5f2bed6b7036f8b35b66dfd4 Mon Sep 17 00:00:00 2001 From: Vonsant Date: Wed, 25 Dec 2024 03:56:12 +0300 Subject: [PATCH 5/5] SmarterBetterStronger --- .../Components/ParrotSpeechComponent.cs | 52 +++- .../EntitySystems/ParrotSpeechSystem.cs | 260 ++++++++++++++++-- .../Entities/Mobs/NPCs/animals.yml | 15 +- .../Mobs/Animals/parrot.rsi/equipped-NECK.png | Bin 0 -> 389 bytes .../Mobs/Animals/parrot.rsi/meta.json | 4 + 5 files changed, 285 insertions(+), 46 deletions(-) create mode 100644 Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/equipped-NECK.png diff --git a/Content.Server/_CorvaxNext/Speech/Components/ParrotSpeechComponent.cs b/Content.Server/_CorvaxNext/Speech/Components/ParrotSpeechComponent.cs index 1ad09fe7067..b41ef786719 100644 --- a/Content.Server/_CorvaxNext/Speech/Components/ParrotSpeechComponent.cs +++ b/Content.Server/_CorvaxNext/Speech/Components/ParrotSpeechComponent.cs @@ -1,40 +1,74 @@ -using Content.Server.Speech.EntitySystems; using Content.Shared.Whitelist; +using Content.Server.Speech.EntitySystems; namespace Content.Server.Speech.Components; +///

+/// This component stores the parrot's learned phrases (both single words and multi-word phrases), +/// and also controls time intervals and learning probabilities. +/// [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. + /// The maximum number of words in a generated phrase if the parrot decides to combine single words. /// [DataField] public int MaximumPhraseLength = 7; + /// + /// The maximum amount of single-word phrases the parrot can store. + /// [DataField] - public int MaximumPhraseCount = 20; + public int MaximumSingleWordCount = 60; + /// + /// The maximum amount of multi-word phrases the parrot can store. + /// [DataField] - public int MinimumWait = 60; // 1 minutes + public int MaximumMultiWordCount = 20; + /// + /// Minimum delay (in seconds) before the next utterance. + /// + [DataField] + public int MinimumWait = 60; // 1 minute + + /// + /// Maximum delay (in seconds) before the next utterance. + /// [DataField] public int MaximumWait = 120; // 2 minutes /// - /// The probability that a parrot will learn from something an overheard phrase. + /// Probability that the parrot learns an overheard phrase. /// [DataField] public float LearnChance = 0.2f; + /// + /// List of entities that are blacklisted from parrot listening. + /// If the entity is in the blacklist, the parrot won't learn from them. + /// [DataField] public EntityWhitelist Blacklist { get; private set; } = new(); - [DataField] - public TimeSpan? NextUtterance; + /// + /// Set of single-word phrases (unique words) the parrot has learned. + /// + [DataField(readOnly: true)] + public HashSet SingleWordPhrases = new(); + /// + /// Set of multi-word phrases (2 or more words) the parrot has learned. + /// [DataField(readOnly: true)] - public List LearnedPhrases = new(); + public HashSet MultiWordPhrases = new(); + + /// + /// The next time the parrot will speak (when the current time is beyond this value). + /// + [DataField] + public TimeSpan? NextUtterance; } diff --git a/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs b/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs index 940a9a72ed5..1516c6c0850 100644 --- a/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs +++ b/Content.Server/_CorvaxNext/Speech/EntitySystems/ParrotSpeechSystem.cs @@ -8,6 +8,10 @@ namespace Content.Server.Speech.EntitySystems; +/// +/// This system handles the learning (when the parrot hears a phrase) and +/// the random utterances (when the parrot speaks). +/// public sealed class ParrotSpeechSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; @@ -20,7 +24,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnListen); - SubscribeLocalEvent(CanListen); + SubscribeLocalEvent(OnListenAttempt); } public override void Update(float frameTime) @@ -28,58 +32,254 @@ 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. + // If the parrot has not learned anything, skip + if (component.SingleWordPhrases.Count == 0 && component.MultiWordPhrases.Count == 0) continue; + + // If parrot is controlled by a player (has Mind), skip if (TryComp(uid, out var mind) && mind.HasMind) - // Pause parrot speech when someone is controlling the parrot. continue; + + // Check the time to speak if (_timing.CurTime < component.NextUtterance) continue; - if (component.NextUtterance is not null) + // Construct a phrase the parrot will say + var phrase = PickRandomPhrase(component); + if (string.IsNullOrWhiteSpace(phrase)) + continue; + + // Send the phrase to the chat system (hidden from chat/log to avoid spam) + _chat.TrySendInGameICMessage(uid, phrase, InGameICChatType.Speak, + hideChat: true, + hideLog: true, + checkRadioPrefix: false); + + // Reset next utterance time + component.NextUtterance = _timing.CurTime + + TimeSpan.FromSeconds(_random.Next(component.MinimumWait, component.MaximumWait)); + } + } + + /// + /// Picks a random phrase to utter. May be a single word, a multi-word phrase, or + /// a combination of single words (up to MaximumPhraseLength). + /// + private string PickRandomPhrase(ParrotSpeechComponent component) + { + var singleCount = component.SingleWordPhrases.Count; + var multiCount = component.MultiWordPhrases.Count; + if (singleCount == 0 && multiCount == 0) + return string.Empty; + + // 1) If we only have single words, use single approach + // 2) If we only have multi-word phrases, use multi approach + // 3) Otherwise, pick randomly among: + // a) Single word + // b) Full multi-word phrase + // c) Combined single words + + bool haveSingle = singleCount > 0; + bool haveMulti = multiCount > 0; + + if (haveSingle && !haveMulti) + { + // Only single words exist + return PickSingleWordOrCombine(component); + } + else if (!haveSingle && haveMulti) + { + // Only multi-word phrases exist + return PickRandomMultiWord(component); + } + else + { + // We have both single and multi, choose approach + var roll = _random.Next(3); // 0..2 + switch (roll) { - _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); + case 0: + // single word + return PickSingleWord(component); + case 1: + // multi-word phrase + return PickRandomMultiWord(component); + default: + // combined single words + return CombineMultipleWords(component); } + } + } - component.NextUtterance = _timing.CurTime + TimeSpan.FromSeconds(_random.Next(component.MinimumWait, component.MaximumWait)); + /// + /// If we only have single words, we can either speak a single one or combine them. + /// + private string PickSingleWordOrCombine(ParrotSpeechComponent component) + { + // 50% chance single word, 50% chance combine + if (_random.Prob(0.5f)) + { + return PickSingleWord(component); } + else + { + return CombineMultipleWords(component); + } + } + + /// + /// Picks a random single word from SingleWordPhrases. + /// + private string PickSingleWord(ParrotSpeechComponent component) + { + var list = component.SingleWordPhrases.ToList(); + return _random.Pick(list); } + /// + /// Picks a random multi-word phrase from MultiWordPhrases. + /// + private string PickRandomMultiWord(ParrotSpeechComponent component) + { + var list = component.MultiWordPhrases.ToList(); + return _random.Pick(list); + } + + /// + /// Combines multiple single words (up to MaximumPhraseLength) into one phrase. + /// The length is random from 1 to max, but not exceeding the total single words we have. + /// + private string CombineMultipleWords(ParrotSpeechComponent component) + { + var countAvailable = component.SingleWordPhrases.Count; + if (countAvailable == 0) + return string.Empty; + + var maxCount = Math.Min(countAvailable, component.MaximumPhraseLength); + var wordsToUse = _random.Next(1, maxCount + 1); + + var list = component.SingleWordPhrases.ToList(); + _random.Shuffle(list); + + var shuffled = list.Take(wordsToUse); + var combined = string.Join(" ", shuffled); + return combined; + } + + /// + /// This event is triggered when the parrot hears someone speaking. If allowed, the parrot may learn it. + /// Now we remove punctuation from the message, split into words, pick a random sub-chunk, and save both: + /// - The whole sub-chunk as single or multi-word phrase + /// - Each word from that sub-chunk as a single word + /// 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); + // Random chance to learn + if (!_random.Prob(component.LearnChance)) + return; - var startIndex = _random.Next(0, Math.Max(0, words.Length - phraseLength + 1)); + // 1) Remove punctuation (replace it with spaces), convert to lower-case + var cleaned = RemovePunctuationAndToLower(args.Message); - var phrase = string.Join(" ", words.Skip(startIndex).Take(phraseLength)).ToLower(); + // 2) Split into words + var words = cleaned.Split(" ", StringSplitOptions.RemoveEmptyEntries); + if (words.Length == 0) + return; - while (component.LearnedPhrases.Count >= component.MaximumPhraseCount) - { - _random.PickAndTake(component.LearnedPhrases); - } + // 3) Decide how many words we pick from the overheard message + var phraseLength = 1 + (int)(Math.Sqrt(_random.NextDouble()) * component.MaximumPhraseLength); + if (phraseLength > words.Length) + phraseLength = words.Length; + + // 4) Pick a random start index + var startIndex = _random.Next(0, Math.Max(1, words.Length - phraseLength + 1)); + var chunk = words.Skip(startIndex).Take(phraseLength).ToArray(); - component.LearnedPhrases.Add(phrase); + // 5) If chunk has only 1 word, store it as single word + // otherwise store it as a multi-word phrase + if (chunk.Length == 1) + { + LearnSingleWord(chunk[0], component); + } + else + { + var phrase = string.Join(" ", chunk); + LearnMultiWord(phrase, component); + } + + // 6) Independently, store all words of that chunk as single words (no duplicates) + foreach (var w in chunk) + { + LearnSingleWord(w, component); } } - private void CanListen(EntityUid uid, ParrotSpeechComponent component, ref ListenAttemptEvent args) + /// + /// Checks if the source is blacklisted. If so, the parrot won't listen. + /// + private void OnListenAttempt(EntityUid uid, ParrotSpeechComponent component, ref ListenAttemptEvent args) { if (_whitelistSystem.IsBlacklistPass(component.Blacklist, args.Source)) args.Cancel(); } + + /// + /// Adds a single word into the SingleWordPhrases set, removing a random word if we exceed the limit. + /// + private void LearnSingleWord(string word, ParrotSpeechComponent component) + { + // If we already have it, skip + if (component.SingleWordPhrases.Contains(word)) + return; + + // If we exceed maximum, remove a random single word + if (component.SingleWordPhrases.Count >= component.MaximumSingleWordCount) + { + var list = component.SingleWordPhrases.ToList(); + var toRemove = _random.Pick(list); + component.SingleWordPhrases.Remove(toRemove); + } + + component.SingleWordPhrases.Add(word); + } + + /// + /// Adds a multi-word phrase into the MultiWordPhrases set, removing a random phrase if we exceed the limit. + /// + private void LearnMultiWord(string phrase, ParrotSpeechComponent component) + { + // If we already have it, skip + if (component.MultiWordPhrases.Contains(phrase)) + return; + + // If we exceed maximum, remove a random multi-word phrase + if (component.MultiWordPhrases.Count >= component.MaximumMultiWordCount) + { + var list = component.MultiWordPhrases.ToList(); + var toRemove = _random.Pick(list); + component.MultiWordPhrases.Remove(toRemove); + } + + component.MultiWordPhrases.Add(phrase); + } + + /// + /// Replaces all punctuation with spaces and returns a lower-cased string. + /// E.g. "Hello, world! I'm here." => "hello world i m here " + /// then trimmed/split => "hello", "world", "i", "m", "here" + /// + private string RemovePunctuationAndToLower(string text) + { + var chars = text.ToCharArray(); + for (var i = 0; i < chars.Length; i++) + { + if (char.IsPunctuation(chars[i])) + { + chars[i] = ' '; + } + } + + // Convert to lower case + return new string(chars).ToLowerInvariant(); + } } diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml index 8ef42e980e0..12bcb79c2c7 100644 --- a/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/_CorvaxNext/Entities/Mobs/NPCs/animals.yml @@ -14,13 +14,13 @@ - 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: SpriteMovement # until upstream + # movementLayers: + # movement: + # state: parrot-moving + # noMovementLayers: + # movement: + # state: parrot-moving - type: Item sprite: _CorvaxNext/Mobs/Animals/parrot.rsi size: Normal @@ -34,6 +34,7 @@ sprite: _CorvaxNext/Mobs/Animals/parrot.rsi slots: - HEAD + - NECK - type: Fixtures fixtures: fix1: diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/equipped-NECK.png b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/equipped-NECK.png new file mode 100644 index 0000000000000000000000000000000000000000..668decbb7e63c19fc6c1fc302b834e2a39d46769 GIT binary patch literal 389 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%z?LA!_Ln`LH zy|vNnaDW8s17X3a9nOvq)jB5dRW>(Vuq>8aIQxm@E{(o-epc2g`=e!RxR+b8u#}q3 z>~~fd_w{nhfBbXW?mOJ--~Ug3%CDfLbm<)Lj`tk-^Xa zv(CNnIdQe*n&$n$+r8KM?ZZF%KH7HG|Biv|gS|TrzWu2zU_Q66_Zicho16mXx4-V+ zbh`98|CbCa2NyU<$zXl)_rxE5>+`HLnph0>Z?-*VDNs6PQ(sWr!wswX-&|na!#>|G zj)6g3?Tzh(>k$nnysia$r91s!HGk^d`Ue(ydyNZu;-nWCRs5@RzE{IGmzi&^5k~-1 zr=nTq>YBH4jcb;~`ZSz6us8nK&kqb6PBMqEl^HT_+O(YStk*lEvU}#cV>a*i77&C2 b$=>{TP4Sn$NaJI literal 0 HcmV?d00001 diff --git a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/meta.json b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/meta.json index e0d80415b48..976e23f348b 100644 --- a/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/meta.json +++ b/Resources/Textures/_CorvaxNext/Mobs/Animals/parrot.rsi/meta.json @@ -31,6 +31,10 @@ "name": "equipped-HELMET", "directions": 4 }, + { + "name": "equipped-NECK", + "directions": 4 + }, { "name": "inhand-left", "directions": 4