args.Handled = true;
}
- private bool ValidAction(BaseActionComponent action, bool canReach = true)
- {
- if (!action.Enabled)
- return false;
-
- if (action.Charges.HasValue && action.Charges <= 0)
- return false;
-
- var curTime = _timing.CurTime;
- if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
- return false;
-
- return canReach || action is BaseTargetActionComponent { CheckCanAccess: false };
- }
-
private List<(EntityUid Id, T Comp)> GetValidActions<T>(List<EntityUid>? actions, bool canReach = true) where T : BaseActionComponent
{
var valid = new List<(EntityUid Id, T Comp)>();
{
if (!_actions.TryGetActionData(id, out var baseAction) ||
baseAction as T is not { } action ||
- !ValidAction(action, canReach))
+ !_actions.ValidAction(action, canReach))
{
continue;
}
--- /dev/null
+using Content.Server.NPC.Systems;
+using Content.Shared.Actions;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.NPC.Components;
+
+/// <summary>
+/// This is used for an NPC that constantly tries to use an action on a given target.
+/// </summary>
+[RegisterComponent, Access(typeof(NPCUseActionOnTargetSystem))]
+public sealed partial class NPCUseActionOnTargetComponent : Component
+{
+ /// <summary>
+ /// HTN blackboard key for the target entity
+ /// </summary>
+ [DataField]
+ public string TargetKey = "Target";
+
+ /// <summary>
+ /// Action that's going to attempt to be used.
+ /// </summary>
+ [DataField(required: true)]
+ public EntProtoId<EntityWorldTargetActionComponent> ActionId;
+
+ [DataField]
+ public EntityUid? ActionEnt;
+}
{"RangedRange", 10f},
{"RotateSpeed", float.MaxValue},
{"VisionRadius", 10f},
+ {"AggroVisionRadius", 10f},
};
/// <summary>
return _blackboard.Remove(key);
}
+ public string GetVisionRadiusKey(IEntityManager entMan)
+ {
+ return TryGetValue<EntityUid>("Target", out _, entMan)
+ ? AggroVisionRadius
+ : VisionRadius;
+ }
+
// I Ummd and Ahhd about using strings vs enums and decided on tags because
// if a fork wants to do their own thing they don't need to touch the enum.
public const string PathfindKey = "MovementPathfind";
public const string RotateSpeed = "RotateSpeed";
- public const string VisionRadius = "VisionRadius";
public const string UtilityTarget = "UtilityTarget";
+ private const string VisionRadius = "VisionRadius";
+ private const string AggroVisionRadius = "AggroVisionRadius";
+
/// <summary>
/// A configurable "order" enum that can be given to an NPC from an external source.
/// </summary>
--- /dev/null
+using Content.Server.NPC.Components;
+using Content.Server.NPC.HTN;
+using Content.Shared.Actions;
+using Robust.Shared.Timing;
+
+namespace Content.Server.NPC.Systems;
+
+public sealed class NPCUseActionOnTargetSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<NPCUseActionOnTargetComponent, MapInitEvent>(OnMapInit);
+ }
+
+ private void OnMapInit(Entity<NPCUseActionOnTargetComponent> ent, ref MapInitEvent args)
+ {
+ ent.Comp.ActionEnt = _actions.AddAction(ent, ent.Comp.ActionId);
+ }
+
+ public bool TryUseTentacleAttack(Entity<NPCUseActionOnTargetComponent?> user, EntityUid target)
+ {
+ if (!Resolve(user, ref user.Comp, false))
+ return false;
+
+ if (!TryComp<EntityWorldTargetActionComponent>(user.Comp.ActionEnt, out var action))
+ return false;
+
+ if (!_actions.ValidAction(action))
+ return false;
+
+ if (action.Event != null)
+ {
+ action.Event.Performer = user;
+ action.Event.Action = user.Comp.ActionEnt.Value;
+ action.Event.Coords = Transform(target).Coordinates;
+ }
+
+ _actions.PerformAction(user,
+ null,
+ user.Comp.ActionEnt.Value,
+ action,
+ action.BaseEvent,
+ _timing.CurTime,
+ false);
+ return true;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ // Tries to use the attack on the current target.
+ var query = EntityQueryEnumerator<NPCUseActionOnTargetComponent, HTNComponent>();
+ while (query.MoveNext(out var uid, out var comp, out var htn))
+ {
+ if (!htn.Blackboard.TryGetValue<EntityUid>(comp.TargetKey, out var target, EntityManager))
+ continue;
+
+ TryUseTentacleAttack((uid, comp), target);
+ }
+ }
+}
}
case TargetDistanceCon:
{
- var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
+ var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
if (!TryComp(targetUid, out TransformComponent? targetXform) ||
!TryComp(owner, out TransformComponent? xform))
}
case TargetInLOSCon:
{
- var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
+ var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
return _examine.InRangeUnOccluded(owner, targetUid, radius + 0.5f, null) ? 1f : 0f;
}
case TargetInLOSOrCurrentCon:
{
- var radius = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
+ var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
const float bufferRange = 0.5f;
if (blackboard.TryGetValue<EntityUid>("Target", out var currentTarget, EntityManager) &&
private void Add(NPCBlackboard blackboard, HashSet<EntityUid> entities, UtilityQuery query)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
- var vision = blackboard.GetValueOrDefault<float>(NPCBlackboard.VisionRadius, EntityManager);
+ var vision = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
switch (query)
{
--- /dev/null
+using Content.Shared.Actions;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Abilities.Goliath;
+
+public sealed partial class GoliathSummonTentacleAction : EntityWorldTargetActionEvent
+{
+ /// <summary>
+ /// The ID of the entity that is spawned.
+ /// </summary>
+ [DataField]
+ public EntProtoId EntityId = "EffectGoliathTentacleSpawn";
+
+ /// <summary>
+ /// Directions determining where the entities will spawn.
+ /// </summary>
+ [DataField]
+ public List<Direction> OffsetDirections = new()
+ {
+ Direction.North,
+ Direction.South,
+ Direction.East,
+ Direction.West,
+ };
+
+ /// <summary>
+ /// How many entities will spawn beyond the original one at the target location?
+ /// </summary>
+ [DataField]
+ public int ExtraSpawns = 3;
+};
--- /dev/null
+using Content.Shared.Directions;
+using Content.Shared.Maps;
+using Content.Shared.Physics;
+using Content.Shared.Popups;
+using Content.Shared.Stunnable;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Network;
+using Robust.Shared.Random;
+
+namespace Content.Shared.Abilities.Goliath;
+
+public sealed class GoliathTentacleSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly SharedMapSystem _map = default!;
+ [Dependency] private readonly SharedStunSystem _stun = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly TurfSystem _turf = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<GoliathSummonTentacleAction>(OnSummonAction);
+ }
+
+ private void OnSummonAction(GoliathSummonTentacleAction args)
+ {
+ if (args.Handled || args.Coords is not { } coords)
+ return;
+
+ // TODO: animation
+
+ _popup.PopupPredicted(Loc.GetString("tentacle-ability-use-popup", ("entity", args.Performer)), args.Performer, args.Performer, type: PopupType.SmallCaution);
+ _stun.TryStun(args.Performer, TimeSpan.FromSeconds(0.8f), false);
+
+ List<EntityCoordinates> spawnPos = new();
+ spawnPos.Add(coords);
+
+ var dirs = new List<Direction>();
+ dirs.AddRange(args.OffsetDirections);
+
+ for (var i = 0; i < 3; i++)
+ {
+ var dir = _random.PickAndTake(dirs);
+ spawnPos.Add(coords.Offset(dir));
+ }
+
+ if (_transform.GetGrid(coords) is not { } grid || !TryComp<MapGridComponent>(grid, out var gridComp))
+ return;
+
+ foreach (var pos in spawnPos)
+ {
+ if (!_map.TryGetTileRef(grid, gridComp, pos, out var tileRef) ||
+ tileRef.IsSpace() ||
+ _turf.IsTileBlocked(tileRef, CollisionGroup.Impassable))
+ {
+ continue;
+ }
+
+ if (_net.IsServer)
+ Spawn(args.EntityId, pos);
+ }
+
+ args.Handled = true;
+ }
+}
// See client-side system for UI code.
}
+ public bool ValidAction(BaseActionComponent action, bool canReach = true)
+ {
+ if (!action.Enabled)
+ return false;
+
+ if (action.Charges.HasValue && action.Charges <= 0)
+ return false;
+
+ var curTime = GameTiming.CurTime;
+ if (action.Cooldown.HasValue && action.Cooldown.Value.End > curTime)
+ return false;
+
+ return canReach || action is BaseTargetActionComponent { CheckCanAccess: false };
+ }
+
#endregion
#region EquipHandlers
using Content.Shared.Standing;
using Content.Shared.StatusEffect;
using Content.Shared.Throwing;
+using Content.Shared.Whitelist;
using Robust.Shared.Audio.Systems;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
namespace Content.Shared.Stunnable;
public abstract class SharedStunSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
+ [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
[Dependency] private readonly StandingStateSystem _standingState = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffect = default!;
SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove);
SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(UpdateCanMove);
+ SubscribeLocalEvent<StunOnContactComponent, ComponentStartup>(OnStunOnContactStartup);
+ SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide);
+
// helping people up if they're knocked down
SubscribeLocalEvent<KnockedDownComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
_blocker.UpdateCanMove(uid);
}
+ private void OnStunOnContactStartup(Entity<StunOnContactComponent> ent, ref ComponentStartup args)
+ {
+ if (TryComp<PhysicsComponent>(ent, out var body))
+ _broadphase.RegenerateContacts(ent, body);
+ }
+
+ private void OnStunOnContactCollide(Entity<StunOnContactComponent> ent, ref StartCollideEvent args)
+ {
+ if (args.OurFixtureId != ent.Comp.FixtureId)
+ return;
+
+ if (_entityWhitelist.IsBlacklistPass(ent.Comp.Blacklist, args.OtherEntity))
+ return;
+
+ if (!TryComp<StatusEffectsComponent>(args.OtherEntity, out var status))
+ return;
+
+ TryStun(args.OtherEntity, ent.Comp.Duration, true, status);
+ TryKnockdown(args.OtherEntity, ent.Comp.Duration, true, status);
+ }
+
private void OnKnockInit(EntityUid uid, KnockedDownComponent component, ComponentInit args)
{
_standingState.Down(uid);
--- /dev/null
+using Content.Shared.Whitelist;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Stunnable;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedStunSystem))]
+public sealed partial class StunOnContactComponent : Component
+{
+ /// <summary>
+ /// The fixture the entity must collide with to be stunned
+ /// </summary>
+ [DataField]
+ public string FixtureId = "fix";
+
+ /// <summary>
+ /// The duration of the stun.
+ /// </summary>
+ [DataField]
+ public TimeSpan Duration = TimeSpan.FromSeconds(5);
+
+ [DataField]
+ public EntityWhitelist Blacklist = new();
+}
--- /dev/null
+tentacle-ability-use-popup = {CAPITALIZE(THE($entity))} digs its tentacles under the ground!
--- /dev/null
+- type: entity
+ id: BaseMobAsteroid
+ parent:
+ - BaseMob
+ - MobDamageable
+ - MobAtmosExposed
+ - MobCombat
+ abstract: true
+ components:
+ - type: Reactive
+ groups:
+ Flammable: [Touch]
+ Extinguish: [Touch]
+ Acidic: [Touch, Ingestion]
+ - type: Body
+ prototype: Animal
+ - type: Climbing
+ - type: NameIdentifier
+ group: GenericNumber
+ - type: StatusEffects
+ allowed:
+ - SlowedDown
+ - Stutter
+ - Stun
+ - Electrocution
+ - TemporaryBlindness
+ - RadiationProtection
+ - Drowsiness
+ - type: StandingState
+ - type: Tag
+ tags:
+ - DoorBumpOpener
+
+- type: entity
+ id: MobGoliath
+ parent: BaseMobAsteroid
+ name: goliath
+ description: A massive beast that uses long tentacles to ensnare its prey, threatening them is not advised under any conditions.
+ components:
+ - type: Sprite
+ sprite: Mobs/Aliens/Asteroid/goliath.rsi
+ layers:
+ - map: ["enum.DamageStateVisualLayers.Base"]
+ state: goliath
+ - type: DamageStateVisuals
+ states:
+ Alive:
+ Base: goliath
+ Dead:
+ Base: goliath_dead
+ - type: MovementSpeedModifier
+ baseWalkSpeed : 2.50
+ baseSprintSpeed : 2.50
+ - type: MobThresholds
+ thresholds:
+ 0: Alive
+ 300: Dead
+ - type: MeleeWeapon
+ soundHit:
+ path: "/Audio/Weapons/smash.ogg"
+ angle: 0
+ attackRate: 0.75
+ animation: WeaponArcPunch
+ damage:
+ types:
+ Slash: 15
+ Piercing: 10
+ - type: NpcFactionMember
+ factions:
+ - SimpleHostile
+ - type: HTN
+ rootTask:
+ task: GoliathCompound
+ blackboard:
+ VisionRadius: !type:Single
+ 6
+ AggroVisionRadius: !type:Single
+ 10
+ - type: NPCUseActionOnTarget
+ actionId: ActionGoliathTentacle
+ - type: Tag
+ tags:
+ - CannotSuicide
+ - Goliath
+ - FootstepSound
+ - type: NoSlip
+ - type: Butcherable
+ spawned:
+ - id: FoodMeatGoliath
+ amount: 3
+ - id: MaterialGoliathHide1
+
+- type: entity
+ id: ActionGoliathTentacle
+ name: "[color=red]Tentacle Slam[/color]"
+ description: Use your tentacles to grab and stun a target player!
+ components:
+ - type: EntityWorldTargetAction
+ raiseOnUser: true
+ icon:
+ sprite: Mobs/Aliens/Asteroid/goliath.rsi
+ state: goliath_tentacle_spawn
+ iconOn:
+ sprite: Mobs/Aliens/Asteroid/goliath.rsi
+ state: goliath_tentacle_wiggle
+ sound:
+ path: "/Audio/Weapons/slash.ogg"
+ event: !type:GoliathSummonTentacleAction
+ useDelay: 8
+ range: 10
+
+- type: entity
+ id: GoliathTentacle
+ name: tentacle
+ components:
+ - type: Transform
+ anchored: True
+ - type: Physics
+ bodyType: Static
+ canCollide: true
+ - type: InteractionOutline
+ - type: Sprite
+ sprite: Mobs/Aliens/Asteroid/goliath.rsi
+ layers:
+ - state: goliath_tentacle_wiggle
+ - type: StunOnContact
+ blacklist:
+ tags:
+ - Goliath
+ - type: Fixtures
+ fixtures:
+ fix:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.45,-0.45,0.45,0.45"
+ mask:
+ - Impassable
+ layer:
+ - Impassable
+ hard: false
+ - type: TimedDespawn #do this shit by hand because of fucking course.
+ lifetime: 0.4
+ - type: SpawnOnDespawn
+ prototype: EffectGoliathTentacleRetract
+
+- type: entity
+ id: BaseEffectGoliathTentacleSpawn
+ categories: [ HideSpawnMenu ]
+ name: tentacle
+ abstract: true
+ components:
+ - type: Transform
+ anchored: True
+ - type: Physics
+ bodyType: Static
+ canCollide: false
+ - type: Sprite
+ sprite: Mobs/Aliens/Asteroid/goliath.rsi
+ - type: InteractionOutline
+ - type: TimedDespawn
+ lifetime: 0.7
+
+- type: entity
+ id: EffectGoliathTentacleSpawn
+ parent: BaseEffectGoliathTentacleSpawn
+ categories: [ HideSpawnMenu ]
+ name: tentacle
+ components:
+ - type: Sprite
+ state: goliath_tentacle_spawn
+ - type: SpawnOnDespawn
+ prototype: GoliathTentacle
+
+- type: entity
+ id: EffectGoliathTentacleRetract
+ parent: BaseEffectGoliathTentacleSpawn
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: Sprite
+ state: goliath_tentacle_retract
+ - type: EffectVisuals
+ - type: AnimationPlayer
flavors:
- banana
- type: Food
- trash:
+ trash:
- TrashBananiumPeel
- type: BadFood
- type: SolutionContainerManager
Gunpowder: 100
- type: Item
size: Tiny
+
+- type: entity
+ parent: MaterialBase
+ id: MaterialGoliathHide
+ name: goliath hide plates
+ description: Pieces of a goliath's rocky hide, these might be able to make your suit a bit more durable to attack from the local fauna.
+ suffix: Full
+ components:
+ - type: Sprite
+ sprite: Objects/Materials/hide.rsi
+ layers:
+ - state: goliath_hide
+ map: [ "base" ]
+ - type: StaticPrice
+ price: 0
+ - type: StackPrice
+ price: 1500
+ - type: Appearance
+ - type: Stack
+ stackType: GoliathHide
+ baseLayer: base
+ layerStates:
+ - goliath_hide
+ - goliath_hide_2
+ - goliath_hide_3
+ - type: Item
+ size: Large
+ shape:
+ - 0,0,2,2
+
+- type: entity
+ parent: MaterialGoliathHide
+ id: MaterialGoliathHide1
+ suffix: 1
+ components:
+ - type: Stack
+ count: 1
--- /dev/null
+- type: htnCompound
+ id: GoliathCompound
+ branches:
+ - tasks:
+ - !type:HTNCompoundTask
+ task: GoliathMeleeCombatPrecondition
+ - tasks:
+ - !type:HTNCompoundTask
+ task: IdleCompound
+
+- type: htnCompound
+ id: GoliathMeleeCombatPrecondition
+ branches:
+ - preconditions:
+ - !type:BuckledPrecondition
+ isBuckled: true
+ tasks:
+ - !type:HTNPrimitiveTask
+ operator: !type:UnbuckleOperator
+ shutdownState: TaskFinished
+
+ - preconditions:
+ - !type:InContainerPrecondition
+ isInContainer: true
+ tasks:
+ - !type:HTNCompoundTask
+ task: EscapeCompound
+
+ - preconditions:
+ - !type:PulledPrecondition
+ isPulled: true
+ tasks:
+ - !type:HTNPrimitiveTask
+ operator: !type:UnPullOperator
+ shutdownState: TaskFinished
+
+ - tasks:
+ - !type:HTNPrimitiveTask
+ operator: !type:UtilityOperator
+ proto: NearbyMeleeTargets
+ - !type:HTNCompoundTask
+ task: GoliathAttackTargetCompound
+
+- type: htnCompound
+ id: GoliathAttackTargetCompound
+ branches:
+ - preconditions:
+ - !type:KeyExistsPrecondition
+ key: Target
+ tasks:
+ - !type:HTNPrimitiveTask
+ operator: !type:MoveToOperator
+ shutdownState: PlanFinished
+ pathfindInPlanning: true
+ removeKeyOnFinish: false
+ targetKey: TargetCoordinates
+ pathfindKey: TargetPathfind
+ rangeKey: MeleeRange
+ - !type:HTNPrimitiveTask
+ operator: !type:JukeOperator
+ jukeType: AdjacentTile
+ - !type:HTNPrimitiveTask
+ operator: !type:MeleeOperator
+ targetKey: Target
+ preconditions:
+ - !type:KeyExistsPrecondition
+ key: Target
+ - !type:TargetInRangePrecondition
+ targetKey: Target
+ rangeKey: MeleeRange
+ services:
+ - !type:UtilityService
+ id: MeleeService
+ proto: NearbyMeleeTargets
+ key: Target
minCount: 5
maxCount: 8
groups:
- - id: MobXeno
+ - id: MobGoliath
amount: 1
#- type: dungeonConfig
minCount: 20
maxCount: 30
groups:
- - id: MobXeno
+ - id: MobGoliath
amount: 1
#- type: dungeonConfig
icon: { sprite: /Textures/Objects/Misc/reagent_fillings.rsi, state: powderpile }
spawn: MaterialGunpowder
maxCount: 60
+
+- type: stack
+ id: GoliathHide
+ name: goliath hide
+ icon: { sprite: /Textures/Objects/Materials/hide.rsi, state: goliath_hide }
+ spawn: MaterialGoliathHide1
+ maxCount: 30
- type: Tag
id: Goat
+- type: Tag
+ id: Goliath
+
- type: Tag
id: GPS
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from Paradise at https://github.com/ParadiseSS13/Paradise/blob/5a68c5f6d3b60ee82c06e0287d1eb8108d2e1fe2/icons/mob/lavaland/lavaland_monsters.dmi",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "goliath",
+ "directions": 4
+ },
+ {
+ "name": "goliath_alert",
+ "directions": 4,
+ "delays": [
+ [
+ 1,
+ 0.2,
+ 1,
+ 0.5
+ ],
+ [
+ 1,
+ 0.2,
+ 1,
+ 0.5
+ ],
+ [
+ 1,
+ 0.2,
+ 1,
+ 0.5
+ ],
+ [
+ 1,
+ 0.2,
+ 1,
+ 0.5
+ ]
+ ]
+ },
+ {
+ "name": "goliath_preattack",
+ "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
+ ],
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ },
+ {
+ "name": "goliath_dead"
+ },
+ {
+ "name": "goliath_tentacle_spawn",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "goliath_tentacle_wiggle",
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "goliath_tentacle_retract",
+ "delays": [
+ [
+ 0.1,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from Paradise at https://github.com/ParadiseSS13/Paradise/blob/88d236d5c91e9a57e103957555c2e9a8c63b68fa/icons/obj/stacks/organic.dmi",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "goliath_hide"
+ },
+ {
+ "name": "goliath_hide_2"
+ },
+ {
+ "name": "goliath_hide_3"
+ }
+ ]
+}
\ No newline at end of file