The Rat King [Antag] (#8706)

* vending machine go spit

* who's da rat, bozo

* fixes

* crown + fixes

* aaaa

* aa

* lololol

* removing vending shit + most annoying fix alive

* paul review

* moony fixes

* sloth review

* Minor diseasesystem fix

* inverse moment

* A

* Also reduce args allocations

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
This commit is contained in:
EmoGarbage404
2022-06-15 20:53:26 -04:00
committed by GitHub
parent e9a6f6dd93
commit 9be066a8cb
25 changed files with 553 additions and 14 deletions

View File

@@ -1,13 +1,14 @@
using System.Linq;
using Content.Shared.Disease;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Disease.Components
{
[RegisterComponent]
/// <summary>
/// Allows the enity to be infected with diseases.
/// Allows the entity to be infected with diseases.
/// Please use only on mobs.
/// </summary>
[RegisterComponent]
public sealed class DiseaseCarrierComponent : Component
{
/// <summary>
@@ -15,22 +16,33 @@ namespace Content.Server.Disease.Components
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public List<DiseasePrototype> Diseases = new();
/// <summary>
/// The carrier's resistance to disease
/// </summary>
[DataField("diseaseResist")]
[ViewVariables(VVAccess.ReadWrite)]
public float DiseaseResist = 0f;
/// <summary>
/// Diseases the carrier has had, used for immunity.
/// <summary>
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public List<DiseasePrototype> PastDiseases = new();
/// <summary>
/// All the diseases the carrier has or has had.
/// Checked against when trying to add a disease
/// <summary>
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public List<DiseasePrototype> AllDiseases => PastDiseases.Concat(Diseases).ToList();
/// <summary>
/// A list of diseases which the entity does not
/// exhibit direct symptoms from. They still transmit
/// these diseases, just without symptoms.
/// </summary>
[ViewVariables, DataField("carrierDiseases", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<DiseasePrototype>))]
public HashSet<string>? CarrierDiseases;
}
}

View File

@@ -96,17 +96,23 @@ namespace Content.Server.Disease
for (var i = 0; i < carrierComp.Diseases.Count; i++) //this is a for-loop so that it doesn't break when new diseases are added
{
var disease = carrierComp.Diseases[i];
var args = new DiseaseEffectArgs(carrierComp.Owner, disease, EntityManager);
disease.Accumulator += frameTime;
if (disease.Accumulator >= disease.TickTime)
if (disease.Accumulator < disease.TickTime) continue;
// if the disease is on the silent disease list, don't do effects
var doEffects = carrierComp.CarrierDiseases?.Contains(disease.ID) != true;
var args = new DiseaseEffectArgs(carrierComp.Owner, disease, EntityManager);
disease.Accumulator -= disease.TickTime;
foreach (var cure in disease.Cures)
{
if (cure.Cure(args))
CureDisease(carrierComp, disease);
}
if (doEffects)
{
disease.Accumulator -= disease.TickTime;
foreach (var cure in disease.Cures)
{
if (cure.Cure(args))
CureDisease(carrierComp, disease);
}
foreach (var effect in disease.Effects)
{
if (_random.Prob(effect.Probability))
@@ -383,6 +389,14 @@ namespace Content.Server.Disease
TryAddDisease(carrier.Owner, disease, carrier);
}
public void TryInfect(DiseaseCarrierComponent carrier, string? disease, float chance = 0.7f, bool forced = false)
{
if (disease == null || !_prototypeManager.TryIndex<DiseasePrototype>(disease, out var d))
return;
TryInfect(carrier, d, chance, forced);
}
/// <summary>
/// Plays a sneeze/cough popup if applicable
/// and then tries to infect anyone in range

View File

@@ -0,0 +1,54 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Disease;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.RatKing
{
[RegisterComponent]
public sealed class RatKingComponent : Component
{
/// <summary>
/// The action for the Raise Army ability
/// </summary>
[DataField("actionRaiseArmy", required: true)]
public InstantAction ActionRaiseArmy = new();
/// <summary>
/// The amount of hunger one use of Raise Army consumes
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("hungerPerArmyUse", required: true)]
public float HungerPerArmyUse = 25f;
/// <summary>
/// The entity prototype of the mob that Raise Army summons
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("armyMobSpawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ArmyMobSpawnId = "MobRatServant";
/// <summary>
/// The action for the Domain ability
/// </summary>
[ViewVariables, DataField("actionDomain", required: true)]
public InstantAction ActionDomain = new();
/// <summary>
/// The amount of hunger one use of Domain consumes
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("hungerPerDomainUse", required: true)]
public float HungerPerDomainUse = 50f;
/// <summary>
/// The disease prototype id that the Domain ability spreads
/// </summary>
[ViewVariables, DataField("domainDiseaseId", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
public string DomainDiseaseId = "Plague";
/// <summary>
/// The range of the Domain ability.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("domainRange")]
public float DomainRange = 3f;
}
};

View File

@@ -0,0 +1,93 @@
using Content.Server.Actions;
using Content.Server.Disease;
using Content.Server.Disease.Components;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
using Content.Shared.Actions;
using Robust.Shared.Player;
namespace Content.Server.RatKing
{
public sealed class RatKingSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly DiseaseSystem _disease = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RatKingComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<RatKingComponent, RatKingRaiseArmyActionEvent>(OnRaiseArmy);
SubscribeLocalEvent<RatKingComponent, RatKingDomainActionEvent>(OnDomain);
}
private void OnStartup(EntityUid uid, RatKingComponent component, ComponentStartup args)
{
_action.AddAction(uid, component.ActionRaiseArmy, null);
_action.AddAction(uid, component.ActionDomain, null);
}
/// <summary>
/// Summons an allied rat servant at the King, costing a small amount of hunger
/// </summary>
private void OnRaiseArmy(EntityUid uid, RatKingComponent component, RatKingRaiseArmyActionEvent args)
{
if (args.Handled)
return;
if (!TryComp<HungerComponent>(uid, out var hunger))
return;
//make sure the hunger doesn't go into the negatives
if (hunger.CurrentHunger < component.HungerPerArmyUse)
{
_popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, Filter.Entities(uid));
return;
}
args.Handled = true;
hunger.CurrentHunger -= component.HungerPerArmyUse;
Spawn(component.ArmyMobSpawnId, Transform(uid).Coordinates); //spawn the little mouse boi
}
/// <summary>
/// Gets all of the nearby disease-carrying entities in a radius
/// and gives them the specified disease. It has a hunger cost as well
/// </summary>
private void OnDomain(EntityUid uid, RatKingComponent component, RatKingDomainActionEvent args)
{
if (args.Handled)
return;
if (!TryComp<HungerComponent>(uid, out var hunger))
return;
//make sure the hunger doesn't go into the negatives
if (hunger.CurrentHunger < component.HungerPerDomainUse)
{
_popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, Filter.Entities(uid));
return;
}
args.Handled = true;
hunger.CurrentHunger -= component.HungerPerDomainUse;
_popup.PopupEntity(Loc.GetString("rat-king-domain-popup"), uid, Filter.Pvs(uid, default, EntityManager));
var tstalker = GetEntityQuery<DiseaseCarrierComponent>();
foreach (var entity in _lookup.GetEntitiesInRange(uid, component.DomainRange)) //go through all of them, filtering only those in range that are not the king itself
{
if (entity == uid)
continue;
if (tstalker.TryGetComponent(entity, out var diseasecomp))
_disease.TryInfect(diseasecomp, component.DomainDiseaseId); //infect them with w/e disease
}
}
}
public sealed class RatKingRaiseArmyActionEvent : InstantActionEvent { };
public sealed class RatKingDomainActionEvent : InstantActionEvent { };
};

View File

@@ -0,0 +1,50 @@
using System.Linq;
using Content.Server.StationEvents.Components;
using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
public sealed class MouseMigration : StationEvent
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
public static List<string> SpawnedPrototypeChoices = new List<string>() //we double up for that ez fake probability
{"MobMouse", "MobMouse1", "MobMouse2", "MobRatServant"};
public override string Name => "MouseMigration";
public override string? StartAnnouncement =>
Loc.GetString("station-event-mouse-migration-announcement");
public override int EarliestStart => 30;
public override int MinimumPlayers => 35; //this just ensures that it doesn't spawn on lowpop maps.
public override float Weight => WeightLow;
public override int? MaxOccurrences => 1;
protected override float StartAfter => 30f;
protected override float EndAfter => 60;
public override void Startup()
{
base.Startup();
var spawnLocations = _entityManager.EntityQuery<VentCritterSpawnLocationComponent, TransformComponent>().ToList();
_random.Shuffle(spawnLocations);
var spawnAmount = _random.Next(7, 15); // A small colony of critters.
for (int i = 0; i < spawnAmount && i < spawnLocations.Count - 1; i++)
{
var spawnChoice = _random.Pick(SpawnedPrototypeChoices);
if (_random.Prob(0.01f) || i == 0) //small chance for multiple, but always at least 1
spawnChoice = "MobRatKing";
_entityManager.SpawnEntity(spawnChoice, spawnLocations[i].Item2.Coordinates);
}
}
}

View File

@@ -0,0 +1,8 @@
rat-king-raise-army-name = Raise Army
rat-king-raise-army-description = Spend some hunger to summon an allied rat to help defend you.
rat-king-domain-name = Rat King's Domain
rat-king-domain-description = Spend some hunger to infect those around you with the plague.
rat-king-domain-popup = A cloud of plague is released into the air!
rat-king-too-hungry = You are too hungry to use this ability!

View File

@@ -0,0 +1 @@
station-event-mouse-migration-announcement = We have detected an oncoming migration of rodents to the station. Please stay out of maintenance tunnels and try and avoid excessive contact.

View File

@@ -168,6 +168,31 @@
- !type:DiseaseReagentCure
reagent: DemonsBlood
- type: disease
id: Plague
name: plague
cureResist: 0.1
effects:
- !type:DiseaseVomit
probability: 0.005
- !type:DiseasePopUp
probability: 0.025
- !type:DiseaseSnough
probability: 0.025
snoughMessage: disease-cough
snoughSound:
collection: Coughs
- !type:DiseaseHealthChange
probability: 0.05
damage:
types:
Poison: 2
cures:
- !type:DiseaseBedrestCure
maxLength: 120
- !type:DiseaseJustWaitCure
maxLength: 240
- type: disease
id: OwOnavirus
name: OwOnavirus

View File

@@ -112,6 +112,19 @@
- type: Clothing
sprite: Clothing/Head/Misc/cone.rsi
- type: entity
parent: ClothingHeadBase
id: ClothingHeadHatFancyCrown
name: fancy crown
description: It smells like dead rat.
components:
- type: Sprite
sprite: Clothing/Head/Misc/fancycrown.rsi
- type: Clothing
sprite: Clothing/Head/Misc/fancycrown.rsi
- type: MobPrice
price: 3000
- type: entity
parent: ClothingHeadBase
id: ClothingHeadHatCatEars

View File

@@ -0,0 +1,189 @@
- type: entity
name: Rat King
id: MobRatKing
parent: SimpleMobBase
description: He's da rat. He make da roolz.
components:
- type: CombatMode
- type: MovementSpeedModifier
baseWalkSpeed : 3.75
baseSprintSpeed : 3.75
- type: UtilityAI
behaviorSets:
- Idle
- UnarmedAttackHostiles
- type: Reactive
groups:
Flammable: [Touch]
Extinguish: [Touch]
- type: AiFactionTag
factions:
- SimpleHostile
- type: Sprite
drawdepth: Mobs
sprite: Mobs/Animals/regalrat.rsi
layers:
- map: ["enum.DamageStateVisualLayers.Base"]
state: regalrat
- type: Physics
bodyType: KinematicController
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeCircle
radius: 0.25
mass: 120
mask:
- MobMask
layer:
- MobLayer
- type: MobState
thresholds:
0: !type:NormalMobState {}
150: !type:CriticalMobState {}
200: !type:DeadMobState {}
- type: MeleeWeapon
range: 1
arcwidth: 0
arc: claw
damage:
types:
Slash: 12
Piercing: 8
- type: Appearance
visuals:
- type: DamageStateVisualizer
rotate: true
normal: regalrat
crit: dead
dead: dead
- type: Puller
- type: GhostTakeoverAvailable
makeSentient: true
name: Rat King
description: You are the Rat King, scavenge food in order to produce rat minions to do your bidding.
rules: You are an antagonist, scavenge, attack, and grow your hoarde!
- type: Tag
tags:
- CannotSuicide
- DoorBumpOpener
- FootstepSound
- type: NoSlip
- type: RatKing
actionRaiseArmy:
useDelay: 4
icon: Interface/Actions/ratKingArmy.png
name: rat-king-raise-army-name
description: rat-king-raise-army-description
itemIconStyle: NoItem
event: !type:RatKingRaiseArmyActionEvent
hungerPerArmyUse: 25
actionDomain:
useDelay: 10
icon: Interface/Actions/ratKingDomain.png
name: rat-king-domain-name
description: rat-king-domain-description
itemIconStyle: NoItem
event: !type:RatKingDomainActionEvent
hungerPerDomainUse: 50
- type: Access #he's so baller he gets his own access. NT got nothing on him
tags:
- Maintenance
- Service
- type: Butcherable
spawned:
- id: ClothingHeadHatFancyCrown #how did that get there?
amount: 1
- type: DiseaseCarrier
carrierDiseases:
- Plague
- type: SlowOnDamage
speedModifierThresholds:
50: 0.9
75: 0.8
100: 0.7
- type: MobPrice
price: 2500 # rat wealth
- type: entity
name: Rat Servant
id: MobRatServant
parent: SimpleMobBase
description: He's da mini rat. He don't make da roolz.
components:
- type: CombatMode
- type: MovementSpeedModifier
baseWalkSpeed : 4
baseSprintSpeed : 4
- type: UtilityAI
behaviorSets:
- Idle
- UnarmedAttackHostiles
- type: Reactive
groups:
Flammable: [Touch]
Extinguish: [Touch]
- type: AiFactionTag
factions:
- SimpleHostile
- type: Sprite
drawdepth: SmallMobs
sprite: Mobs/Animals/mouse.rsi
layers:
- map: ["enum.DamageStateVisualLayers.Base"]
state: mouse-3
- type: Physics
bodyType: KinematicController
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeCircle
radius: 0.2
mass: 10
mask:
- SmallMobMask
layer:
- SmallMobLayer
- type: MobState
thresholds:
0: !type:NormalMobState {}
25: !type:CriticalMobState {}
50: !type:DeadMobState {}
- type: MeleeWeapon
range: 1
arcwidth: 0
arc: claw
damage:
types:
Slash: 5
Piercing: 2
- type: Appearance
visuals:
- type: DamageStateVisualizer
rotate: true
normal: mouse-3
crit: dead-3
dead: splat-3
- type: Puller
- type: DiseaseCarrier
carrierDiseases:
- Plague
- type: Vocal
# mice are gender neutral who cares
maleScream: /Audio/Animals/mouse_squeak.ogg
femaleScream: /Audio/Animals/mouse_squeak.ogg
wilhelmProbability: 0.001
- type: GhostTakeoverAvailable
makeSentient: true
name: Rat Servant
description: You are a Rat Servant. You must follow your king's orders.
rules: You are an antagonist, scavenge, attack, and serve your king!
- type: Tag
tags:
- CannotSuicide
- DoorBumpOpener
- FootstepSound
- type: NoSlip
- type: MobPrice
price: 500 # rat wealth

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

View File

@@ -0,0 +1,18 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "equipped-HELMET",
"directions": 4
}
]
}

View File

@@ -24,6 +24,12 @@
},
{
"name": "manifest"
}
},
{
"name": "ratKingArmy"
},
{
"name": "ratKingDomain"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -15,6 +15,9 @@
},
{
"name": "icon-2"
},
{
"name": "icon-3"
},
{
"name": "mouse-0",
@@ -93,6 +96,32 @@
0.2
]
]
},
{
"name": "mouse-3",
"directions": 4,
"delays": [
[
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2
],
[
0.2,
0.2,
0.2
]
]
},
{
"name": "dead-0"
@@ -102,6 +131,9 @@
},
{
"name": "dead-2"
},
{
"name": "dead-3"
},
{
"name": "splat-0"
@@ -111,6 +143,9 @@
},
{
"name": "splat-2"
},
{
"name": "splat-3"
},
{
"name": "0-equipped-HELMET",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,21 @@
{
"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": "dead"
},
{
"name": "icon"
},
{
"name": "regalrat",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB