Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Headcrabs #190

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Content.Server/_White/Headcrab/HeadcrabComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;

namespace Content.Server._White.Headcrab;

[Access(typeof(HeadcrabSystem))]
[RegisterComponent]
public sealed partial class HeadcrabComponent : Component
{
/// <summary>
/// WorldTargetAction
/// </summary>
[DataField("jumpAction", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string JumpAction = "ActionHeadcrabJump";

[DataField("paralyzeTime"), ViewVariables(VVAccess.ReadWrite)]
public float ParalyzeTime = 3f;
PuroSlavKing marked this conversation as resolved.
Show resolved Hide resolved

[DataField("chancePounce"), ViewVariables(VVAccess.ReadWrite)]
public int ChancePounce = 33;

[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;

public bool IsDead = false;

public EntityUid EquippedOn;

[ViewVariables] public float Accumulator = 0;

[DataField("damageFrequency"), ViewVariables(VVAccess.ReadWrite)]
public float DamageFrequency = 5;

[ViewVariables(VVAccess.ReadWrite), DataField("jumpSound")]
public SoundSpecifier? HeadcrabJumpSound = new SoundPathSpecifier("/Audio/_White/Misc/Headcrab/headcrab_jump.ogg");

}
256 changes: 256 additions & 0 deletions Content.Server/_White/Headcrab/HeadcrabSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
using System.Linq;
using Content.Server.Actions;
using Content.Server.NPC.Components;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Popups;
using Content.Server.NPC.Components;
using Content.Server.NPC.Systems;
using Content.Server._White.Headcrab;
using Content.Server.Zombies;
using Content.Shared.Zombies;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Hands;
using Content.Shared.Humanoid;
using Content.Shared.Nutrition.Components;
using Content.Shared._White.Headcrab;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;

namespace Content.Server._White.Headcrab;

public sealed partial class HeadcrabSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly ActionsSystem _action = default!;
[Dependency] private readonly IRobustRandom _random = default!;

public override void Initialize()
{
SubscribeLocalEvent<HeadcrabComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<HeadcrabComponent, MeleeHitEvent>(OnMeleeHit);
SubscribeLocalEvent<HeadcrabComponent, ThrowDoHitEvent>(OnHeadcrabDoHit);
SubscribeLocalEvent<HeadcrabComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<HeadcrabComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<HeadcrabComponent, GotEquippedHandEvent>(OnGotEquippedHand);
SubscribeLocalEvent<HeadcrabComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<HeadcrabComponent, BeingUnequippedAttemptEvent>(OnUnequipAttempt);
SubscribeLocalEvent<HeadcrabComponent, JumpActionEvent>(OnJump);
}

private void OnStartup(EntityUid uid, HeadcrabComponent component, ComponentStartup args)
{
_action.AddAction(uid, component.JumpAction);
}

private void OnHeadcrabDoHit(EntityUid uid, HeadcrabComponent component, ThrowDoHitEvent args)
{
if (component.IsDead)
return;
if (HasComp<ZombieComponent>(args.Target))
return;
if (!HasComp<HumanoidAppearanceComponent>(args.Target))
return;
if (TryComp(args.Target, out MobStateComponent? mobState))
{
if (mobState.CurrentState is not MobState.Alive)
{
return;
}
}

_inventory.TryGetSlotEntity(args.Target, "head", out var headItem);
if (HasComp<IngestionBlockerComponent>(headItem))
return;

if (!_inventory.TryEquip(args.Target, uid, "mask", true))
return;

component.EquippedOn = args.Target;

_popup.PopupEntity(Loc.GetString("headcrab-hit-entity-head"),
args.Target, args.Target, PopupType.LargeCaution);

_popup.PopupEntity(Loc.GetString("headcrab-hit-entity-head",
("entity", args.Target)),
uid, uid, PopupType.LargeCaution);

_popup.PopupEntity(Loc.GetString("headcrab-eat-other-entity-face",
("entity", args.Target)), args.Target, Filter.PvsExcept(uid), true, PopupType.Large);

EnsureComp<PacifiedComponent>(uid);
_stunSystem.TryParalyze(args.Target, TimeSpan.FromSeconds(component.ParalyzeTime), true);
_damageableSystem.TryChangeDamage(args.Target, component.Damage);
PuroSlavKing marked this conversation as resolved.
Show resolved Hide resolved
}

private void OnGotEquipped(EntityUid uid, HeadcrabComponent component, GotEquippedEvent args)
{
if (args.Slot != "mask")
return;
component.EquippedOn = args.Equipee;
EnsureComp<PacifiedComponent>(uid);
_npcFaction.AddFaction(uid, "Zombie");
}

private void OnUnequipAttempt(EntityUid uid, HeadcrabComponent component, BeingUnequippedAttemptEvent args)
{
if (args.Slot != "mask")
return;
if (component.EquippedOn != args.Unequipee)
return;
if (HasComp<ZombieComponent>(args.Unequipee))
return;
_popup.PopupEntity(Loc.GetString("headcrab-try-unequip"),
args.Unequipee, args.Unequipee, PopupType.Large);
args.Cancel();
}

private void OnGotEquippedHand(EntityUid uid, HeadcrabComponent component, GotEquippedHandEvent args)
{
if (HasComp<ZombieComponent>(args.User))
return;
if (component.IsDead)
return;
// _handsSystem.TryDrop(args.User, uid, checkActionBlocker: false);
_damageableSystem.TryChangeDamage(args.User, component.Damage);
_popup.PopupEntity(Loc.GetString("headcrab-entity-bite"),
args.User, args.User);
}

private void OnGotUnequipped(EntityUid uid, HeadcrabComponent component, GotUnequippedEvent args)
{
if (args.Slot != "mask")
return;
component.EquippedOn = EntityUid.Invalid;
RemCompDeferred<PacifiedComponent>(uid);
var combatMode = EnsureComp<CombatModeComponent>(uid);
_combat.SetInCombatMode(uid, true, combatMode);
EnsureComp<NPCMeleeCombatComponent>(uid);
_npcFaction.RemoveFaction(uid, "Zombie");
}

private void OnMeleeHit(EntityUid uid, HeadcrabComponent component, MeleeHitEvent args)
{
if (!args.HitEntities.Any())
return;

foreach (var entity in args.HitEntities)
{
if (!HasComp<HumanoidAppearanceComponent>(entity))
return;

if (TryComp(entity, out MobStateComponent? mobState))
{
if (mobState.CurrentState is not MobState.Alive)
{
return;
}
}

_inventory.TryGetSlotEntity(entity, "head", out var headItem);
if (HasComp<IngestionBlockerComponent>(headItem))
return;

var shouldEquip = _random.Next(1, 101) <= component.ChancePounce;
if (!shouldEquip)
return;

var equipped = _inventory.TryEquip(entity, uid, "mask", true);
if (!equipped)
return;

component.EquippedOn = entity;

_popup.PopupEntity(Loc.GetString("headcrab-eat-entity-face"),
entity, entity, PopupType.LargeCaution);

_popup.PopupEntity(Loc.GetString("headcrab-hit-entity-head", ("entity", entity)),
uid, uid, PopupType.LargeCaution);

_popup.PopupEntity(Loc.GetString("headcrab-eat-other-entity-face",
("entity", entity)), entity, Filter.PvsExcept(entity), true, PopupType.Large);

EnsureComp<PacifiedComponent>(uid);
_stunSystem.TryParalyze(entity, TimeSpan.FromSeconds(component.ParalyzeTime), true);
_damageableSystem.TryChangeDamage(entity, component.Damage, origin: entity);
break;
}
PuroSlavKing marked this conversation as resolved.
Show resolved Hide resolved
}

private static void OnMobStateChanged(EntityUid uid, HeadcrabComponent component, MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Dead)
{
component.IsDead = true;
}
}
private void OnJump(EntityUid uid, HeadcrabComponent component, JumpActionEvent args)
{
if (args.Handled)
return;

args.Handled = true;
var xform = Transform(uid);
var mapCoords = _transform.ToMapCoordinates(args.Target);
var direction = mapCoords.Position - _transform.GetMapCoordinates(xform).Position;

_throwing.TryThrow(uid, direction, 7F, uid, 10F);
if (component.HeadcrabJumpSound != null)
{
_audioSystem.PlayPvs(component.HeadcrabJumpSound, uid, component.HeadcrabJumpSound.Params);
}
PuroSlavKing marked this conversation as resolved.
Show resolved Hide resolved
}

public override void Update(float frameTime)
{
base.Update(frameTime);

foreach (var comp in EntityQuery<HeadcrabComponent>())
{
comp.Accumulator += frameTime;

if (comp.Accumulator <= comp.DamageFrequency)
continue;

comp.Accumulator = 0;

if (comp.EquippedOn is not { Valid: true } targetId)
continue;
if (HasComp<ZombieComponent>(comp.EquippedOn))
return;
if (TryComp(targetId, out MobStateComponent? mobState))
{
if (mobState.CurrentState is not MobState.Alive)
{
_inventory.TryUnequip(targetId, "mask", true, true);
comp.EquippedOn = EntityUid.Invalid;
return;
}
}
_damageableSystem.TryChangeDamage(targetId, comp.Damage);
_popup.PopupEntity(Loc.GetString("headcrab-eat-entity-face"),
targetId, targetId, PopupType.LargeCaution);
_popup.PopupEntity(Loc.GetString("headcrab-eat-other-entity-face",
("entity", targetId)), targetId, Filter.PvsExcept(targetId), true);
}
}
}
10 changes: 10 additions & 0 deletions Content.Shared/_White/Headcrab/SharedHeadcrab.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;

namespace Content.Shared._White.Headcrab;

public sealed partial class JumpActionEvent : WorldTargetActionEvent
{

}
Binary file not shown.
11 changes: 11 additions & 0 deletions Resources/Locale/en-US/_white/popups/headcrab.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ghost-role-information-headcrab-name = Headcrab
ghost-role-information-headcrab-description = Ride the carrier, bite off its head and protect your nest.

headcrab-eat-entity-face = Headcrab is eating your head!
headcrab-eat-other-entity-face = Headcrab is eating { CAPITALIZE($entity) } head!

headcrab-hit-entity-head = You got { CAPITALIZE($entity) } head!

headcrab-entity-bite = Headcrab bit your hand!

headcrab-try-unequip = You cant take headcrab off your head.
2 changes: 2 additions & 0 deletions Resources/Locale/en-US/_white/prototypes/actions/types.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
jump-action-name = Jump
jump-action-description = Do super jump.
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
station-event-stray-supply-pod-announcement = Stray capsule with goods was found on long-range scanners. The approximate landing location is unknown.
station-event-stray-supply-pod-syndicate-announcement = Stray capsule with goods was found on long-range scanners. The approximate landing location is unknown.

station-event-headcrab-invasion-announcement = Confirmed sightings of hostile wildlife on the station. Personnel are advised to arm themselves, barricade doors, and defend themselves if necessary. Security is advised to eradicate the threat as soon as possible.
5 changes: 5 additions & 0 deletions Resources/Locale/ru-RU/_white/guidbook/antagonist.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
antagonist-name-headcrabs = Хедкрабы

antagonist-name-headcrab = Хедкраб
antagonist-name-headcrab-fast = Быстрый хедкраб
antagonist-name-headcrab-poison = Ядовитый хедкраб
3 changes: 3 additions & 0 deletions Resources/Locale/ru-RU/_white/guidbook/main.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
guide-entry-wwdp = WWDP

guide-entry-antagonist = Антагонисты
11 changes: 11 additions & 0 deletions Resources/Locale/ru-RU/_white/popups/headcrab.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ghost-role-information-headcrab-name = Хедкраб
ghost-role-information-headcrab-description = Оседлай носителя, откуси ему голову и защити своё гнездо.

headcrab-eat-entity-face = Хедкраб пожирает твою голову!
headcrab-eat-other-entity-face = Хедкраб ест голову { CAPITALIZE($entity) }!

headcrab-hit-entity-head = Вы ухватились за голову { CAPITALIZE($entity) }!

headcrab-entity-bite = Хедкраб укусил вас за руку!

headcrab-try-unequip = Вы не можете снять хедкраба со своей головы.
3 changes: 3 additions & 0 deletions Resources/Locale/ru-RU/_white/prototypes/actions/types.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ ent-ToggleNightVision = Переключить ночное зрение

ent-ToggleThermalVision = Переключить термальное зрение
.desc = Переключает термальное зрение.

jump-action-name = Прыгнуть
jump-action-description = Сделать крутой прыжок.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ent-SpawnHeadcrabNest = спавнер гнездо хедкраба

ent-SpawnMobHeadcrab = спавнер хедкраб
ent-SpawnMobHeadcrabFast = спавнер быстрый хедкраб
ent-SpawnMobHeadcrabPoison = спавнер ядовитый хедкраб
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ent-BaseMobHeadcrab = хедкраб
.desc = Похоже на что-то паразитирующее.

ent-MobHeadcrab = { ent-BaseMobHeadcrab }
.desc = { ent-BaseMobHeadcrab.desc }
ent-MobHeadcrabFast = быстрый хедкраб
.desc = { ent-BaseMobHeadcrab.desc }
ent-MobHeadcrabPoison = ядовитый хедкраб
.desc = { ent-BaseMobHeadcrab.desc }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ent-HeadcrabNest = гнездо хедкрабов
.desc = Кажется оно шевелится.
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
station-event-stray-supply-pod-announcement = На сканерах дальнего действия обнаружена бродячая капсула с товарами. Примерное место приземления неизвестно.
station-event-stray-supply-pod-syndicate-announcement = На сканерах дальнего действия обнаружена бродячая капсула с товарами. Примерное место приземления неизвестно.

station-event-headcrab-invasion-announcement = Подтверждено наличие на станции враждебной фауны. Персоналу рекомендуется вооружиться, забаррикадировать двери и защищаться в случае необходимости. Службе безопасности следует как можно скорее ликвидировать угрозу.
Loading
Loading