Carp Queen (#3352)
28
Content.Client/_Sunrise/CarpQueen/CarpEggVisualizerSystem.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using Content.Shared._Sunrise.CarpQueen;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client._Sunrise.CarpQueen;
|
||||
|
||||
public sealed class CarpEggVisualizerSystem : VisualizerSystem<CarpEggComponent>
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, CarpEggComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if (!args.AppearanceData.TryGetValue(CarpEggVisuals.OverlayColor, out var obj))
|
||||
return;
|
||||
|
||||
var color = (Color) obj;
|
||||
var sprite = args.Sprite;
|
||||
if (_sprite.LayerMapTryGet((uid, sprite), "overlay", out var layer, false))
|
||||
{
|
||||
_sprite.LayerSetColor((uid, sprite), layer, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using Content.Shared._Sunrise.CarpQueen;
|
||||
using Content.Shared.Light.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client._Sunrise.CarpQueen;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side system that applies the liquid color to carp servants
|
||||
/// based on the color of the liquid they hatched from.
|
||||
/// Overrides RgbLightController behavior to use fixed color.
|
||||
/// </summary>
|
||||
public sealed class CarpServantVisualizerSystem : VisualizerSystem<CarpServantMemoryComponent>
|
||||
{
|
||||
[Dependency] private readonly SharedPointLightSystem _lights = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CarpServantMemoryComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<CarpServantMemoryComponent, ComponentAdd>(OnComponentAdd);
|
||||
}
|
||||
|
||||
private void OnComponentAdd(EntityUid uid, CarpServantMemoryComponent component, ComponentAdd args)
|
||||
{
|
||||
// Remove RgbLightController on client side to prevent rainbow effect
|
||||
RemComp<RgbLightControllerComponent>(uid);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
// Continuously override RgbLightController color with fixed liquid color
|
||||
var query = EntityQueryEnumerator<CarpServantMemoryComponent, SpriteComponent>();
|
||||
while (query.MoveNext(out var uid, out var memory, out var sprite))
|
||||
{
|
||||
var color = memory.LiquidColor;
|
||||
|
||||
// Override sprite layer 0 color (base layer)
|
||||
_sprite.LayerSetColor((uid, sprite), 0, color);
|
||||
|
||||
// Override light color if present
|
||||
if (TryComp<PointLightComponent>(uid, out var light))
|
||||
{
|
||||
_lights.SetColor(uid, color, light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, CarpServantMemoryComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
// Apply color to sprite layers
|
||||
var color = component.LiquidColor;
|
||||
var sprite = args.Sprite;
|
||||
|
||||
// Apply color to base layer (layer 0)
|
||||
_sprite.LayerSetColor((uid, sprite), 0, color);
|
||||
|
||||
// Also update light color if present
|
||||
if (TryComp<PointLightComponent>(uid, out var light))
|
||||
{
|
||||
_lights.SetColor(uid, color, light);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, CarpServantMemoryComponent component, ComponentStartup args)
|
||||
{
|
||||
// Apply color immediately on startup
|
||||
if (TryComp<SpriteComponent>(uid, out var sprite))
|
||||
{
|
||||
_sprite.LayerSetColor((uid, sprite), 0, component.LiquidColor);
|
||||
}
|
||||
|
||||
if (TryComp<PointLightComponent>(uid, out var light))
|
||||
{
|
||||
_lights.SetColor(uid, component.LiquidColor, light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
545
Content.Server/_Sunrise/CarpQueen/CarpEggSystem.cs
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
using System.Numerics;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.NPC;
|
||||
using Content.Server.NPC.HTN;
|
||||
using Content.Server.NPC.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared._Sunrise.CarpQueen;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.RatKing;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.NPC.Components;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Server._Sunrise.CarpQueen;
|
||||
|
||||
public sealed class CarpEggSystem : CarpQueenAccessSystem
|
||||
{
|
||||
[Dependency] private readonly PuddleSystem _puddles = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protos = default!;
|
||||
[Dependency] private readonly IRobustRandom _rand = default!;
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly SharedMapSystem _map = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containers = default!;
|
||||
[Dependency] private readonly HTNSystem _htn = default!;
|
||||
[Dependency] private readonly CarpQueenSystem _carpQueenSystem = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _lights = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedDestructibleSystem _destructible = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CarpEggComponent, DestructionEventArgs>(OnEggDestroyed);
|
||||
SubscribeLocalEvent<CarpEggComponent, ComponentShutdown>(OnEggShutdown);
|
||||
SubscribeLocalEvent<CarpEggComponent, MapInitEvent>(OnEggMapInit);
|
||||
SubscribeLocalEvent<CarpEggComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
SubscribeLocalEvent<CarpEggComponent, EntGotRemovedFromContainerMessage>(OnRemovedFromContainer);
|
||||
SubscribeLocalEvent<SolutionChangedEvent>(OnSolutionChanged);
|
||||
SubscribeLocalEvent<CarpQueenServantComponent, ComponentStartup>(OnServantStartup);
|
||||
SubscribeLocalEvent<PuddleComponent, MapInitEvent>(OnPuddleMapInit);
|
||||
SubscribeLocalEvent<TileChangedEvent>(OnTileChanged);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<CarpEggComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var egg, out var xform))
|
||||
{
|
||||
// If not currently eligible, periodically re-check conditions to become eligible
|
||||
if (!egg.Eligible)
|
||||
{
|
||||
egg.Accum += frameTime;
|
||||
egg.WaitElapsed += frameTime;
|
||||
if (egg.Accum >= egg.CheckInterval)
|
||||
{
|
||||
egg.Accum = 0f;
|
||||
TryHatchCheck(uid, egg);
|
||||
}
|
||||
|
||||
// If waited too long without liquid, destroy the egg (same as if it was broken)
|
||||
if (egg.WaitElapsed >= egg.MaxWaitWithoutLiquid)
|
||||
{
|
||||
_destructible.DestroyEntity(uid);
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Eligible: count down to hatch
|
||||
egg.Accum += frameTime;
|
||||
if (egg.Accum >= egg.HatchDelay)
|
||||
{
|
||||
// Validate still on liquid before hatching
|
||||
if (TryComp<MapGridComponent>(xform.GridUid, out var grid))
|
||||
{
|
||||
var tile = _map.GetTileRef(xform.GridUid.Value, grid, xform.Coordinates);
|
||||
if (HasSufficientLiquid(tile, egg.RequiredVolume))
|
||||
{
|
||||
Hatch(uid, egg, xform);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Conditions no longer valid
|
||||
egg.Eligible = false;
|
||||
egg.Accum = 0f;
|
||||
// Do not reset WaitElapsed here so total wait continues accumulating until liquid appears
|
||||
ResetVisual(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnServantStartup(EntityUid uid, CarpQueenServantComponent servant, ComponentStartup args)
|
||||
{
|
||||
// Queen present: follow and use her current orders
|
||||
if (servant.Queen != null && TryComp(servant.Queen.Value, out CarpQueenComponent? queen))
|
||||
{
|
||||
_npc.SetBlackboard(uid, NPCBlackboard.FollowTarget, new EntityCoordinates(servant.Queen.Value, Vector2.Zero));
|
||||
// Convert CarpQueenOrderType to RatKingOrderType for HTN compatibility
|
||||
var ratKingOrder = SharedCarpQueenSystem.ConvertToRatKingOrder(queen.CurrentOrder);
|
||||
_npc.SetBlackboard(uid, NPCBlackboard.CurrentOrders, ratKingOrder);
|
||||
_npc.SetBlackboard(uid, "FollowCloseRange", 1.0f);
|
||||
_npc.SetBlackboard(uid, "FollowRange", 1.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No queen: default to Loose so directly spawned servants are active
|
||||
// Convert to RatKingOrderType for HTN compatibility
|
||||
_npc.SetBlackboard(uid, NPCBlackboard.CurrentOrders, RatKingOrderType.Loose);
|
||||
}
|
||||
|
||||
// If HTN is already present, force a replan now
|
||||
if (TryComp<HTNComponent>(uid, out var htn))
|
||||
{
|
||||
if (htn.Plan != null)
|
||||
_htn.ShutdownPlan(htn);
|
||||
_htn.Replan(htn);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEggMapInit(EntityUid uid, CarpEggComponent egg, MapInitEvent args)
|
||||
{
|
||||
// Defer until queen is assigned to avoid spawning unlinked servants
|
||||
if (egg.Queen == null)
|
||||
return;
|
||||
|
||||
TryHatchCheck(uid, egg);
|
||||
}
|
||||
|
||||
private void OnAnchorChanged(EntityUid uid, CarpEggComponent egg, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
if (args.Anchored)
|
||||
TryHatchCheck(uid, egg);
|
||||
}
|
||||
|
||||
private void OnRemovedFromContainer(EntityUid uid, CarpEggComponent egg, EntGotRemovedFromContainerMessage args)
|
||||
{
|
||||
TryHatchCheck(uid, egg);
|
||||
}
|
||||
|
||||
private void OnSolutionChanged(ref SolutionChangedEvent args)
|
||||
{
|
||||
// If a puddle changed, re-check eggs on that tile
|
||||
// Skip if entity is being deleted or doesn't have required components
|
||||
if (!TryComp<PuddleComponent>(args.Solution.Owner, out var _))
|
||||
return;
|
||||
|
||||
// Additional safety check - ensure entity is valid
|
||||
if (TerminatingOrDeleted(args.Solution.Owner))
|
||||
return;
|
||||
|
||||
var xform = Transform(args.Solution.Owner);
|
||||
if (xform.GridUid == null)
|
||||
return;
|
||||
if (!TryComp<MapGridComponent>(xform.GridUid.Value, out var grid))
|
||||
return;
|
||||
var tile = _map.GetTileRef(xform.GridUid.Value, grid, xform.Coordinates);
|
||||
foreach (var ent in _lookup.GetEntitiesInTile(tile))
|
||||
{
|
||||
if (TryComp<CarpEggComponent>(ent, out var egg))
|
||||
TryHatchCheck(ent, egg);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPuddleMapInit(EntityUid uid, PuddleComponent puddle, MapInitEvent args)
|
||||
{
|
||||
// New puddle spawned: check eggs on this tile
|
||||
var xform = Transform(uid);
|
||||
if (xform.GridUid == null)
|
||||
return;
|
||||
if (!TryComp<MapGridComponent>(xform.GridUid.Value, out var grid))
|
||||
return;
|
||||
var tile = _map.GetTileRef(xform.GridUid.Value, grid, xform.Coordinates);
|
||||
foreach (var ent in _lookup.GetEntitiesInTile(tile))
|
||||
{
|
||||
if (TryComp<CarpEggComponent>(ent, out var egg))
|
||||
TryHatchCheck(ent, egg);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTileChanged(ref TileChangedEvent ev)
|
||||
{
|
||||
if (!TryComp<MapGridComponent>(ev.Entity, out var grid))
|
||||
return;
|
||||
|
||||
foreach (var change in ev.Changes)
|
||||
{
|
||||
var tile = _map.GetTileRef(ev.Entity, grid, change.GridIndices);
|
||||
foreach (var ent in _lookup.GetEntitiesInTile(tile))
|
||||
{
|
||||
if (TryComp<CarpEggComponent>(ent, out var egg))
|
||||
TryHatchCheck(ent, egg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void TryHatchCheck(EntityUid uid, CarpEggComponent egg)
|
||||
{
|
||||
if (!TryComp<TransformComponent>(uid, out var xform))
|
||||
return;
|
||||
|
||||
// Only hatch if not inside containers
|
||||
if (xform.GridUid == null)
|
||||
return;
|
||||
if (_containers.IsEntityInContainer(uid))
|
||||
return;
|
||||
|
||||
if (!TryComp<MapGridComponent>(xform.GridUid.Value, out var grid))
|
||||
return;
|
||||
var tile = _map.GetTileRef(xform.GridUid.Value, grid, xform.Coordinates);
|
||||
|
||||
if (HasSufficientLiquid(tile, egg.RequiredVolume))
|
||||
{
|
||||
if (!egg.Eligible)
|
||||
{
|
||||
egg.Eligible = true;
|
||||
egg.Accum = 0f;
|
||||
egg.WaitElapsed = 0f;
|
||||
// Show popup locally to queen if present, otherwise to all nearby
|
||||
if (egg.Queen != null && Exists(egg.Queen.Value))
|
||||
_popup.PopupEntity(Loc.GetString("carp-egg-activates"), uid, egg.Queen.Value);
|
||||
else
|
||||
_popup.PopupEntity(Loc.GetString("carp-egg-activates"), uid);
|
||||
}
|
||||
UpdateVisualForTile(uid, tile);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (egg.Eligible)
|
||||
{
|
||||
egg.Eligible = false;
|
||||
egg.Accum = 0f;
|
||||
ResetVisual(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasSufficientLiquid(TileRef tile, float required)
|
||||
{
|
||||
// Puddle volume check
|
||||
if (_puddles.TryGetPuddle(tile, out var puddle))
|
||||
{
|
||||
var vol = _puddles.CurrentVolume(puddle);
|
||||
if (vol >= FixedPoint2.New(required))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Floor water entity check counts as sufficient
|
||||
var gridId = tile.GridUid;
|
||||
if (gridId != null)
|
||||
{
|
||||
// Check anchored entities first
|
||||
if (gridId is { } gid && TryComp<MapGridComponent>(gid, out var grid))
|
||||
{
|
||||
var enumerator = _map.GetAnchoredEntitiesEnumerator(gid, grid, tile.GridIndices);
|
||||
while (enumerator.MoveNext(out EntityUid? ent))
|
||||
{
|
||||
if (!ent.HasValue)
|
||||
continue;
|
||||
|
||||
// Check by prototype ID
|
||||
var meta = MetaData(ent.Value);
|
||||
if (meta.EntityPrototype?.ID == "FloorWaterEntity")
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check all entities in tile (fallback)
|
||||
var entities = _lookup.GetEntitiesInTile(tile);
|
||||
foreach (var ent in entities)
|
||||
{
|
||||
var meta = MetaData(ent);
|
||||
if (meta.EntityPrototype?.ID == "FloorWaterEntity")
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateVisualForTile(EntityUid uid, TileRef tile)
|
||||
{
|
||||
Color color;
|
||||
// Prefer puddle solution color if present
|
||||
if (_puddles.TryGetPuddle(tile, out var puddle) && TryComp(puddle, out PuddleComponent? puddleComp) && puddleComp.Solution != null)
|
||||
{
|
||||
var sol = puddleComp.Solution.Value.Comp.Solution;
|
||||
color = sol.GetColor(_protos);
|
||||
}
|
||||
else
|
||||
{
|
||||
// FloorWaterEntity fallback -> use Water reagent color
|
||||
color = _protos.Index<ReagentPrototype>("Water").SubstanceColor;
|
||||
}
|
||||
|
||||
// Tint light only on server; sprite tint is clientside visualizer concern
|
||||
_lights.SetColor(uid, color);
|
||||
_appearance.SetData(uid, CarpEggVisuals.OverlayColor, color);
|
||||
}
|
||||
|
||||
private void ResetVisual(EntityUid uid)
|
||||
{
|
||||
// Reset to white
|
||||
_lights.SetColor(uid, Color.White);
|
||||
_appearance.SetData(uid, CarpEggVisuals.OverlayColor, Color.White);
|
||||
}
|
||||
|
||||
private void Hatch(EntityUid uid, CarpEggComponent egg, TransformComponent xform)
|
||||
{
|
||||
// Get tile information for liquid color and reagents
|
||||
Color liquidColor = Color.White;
|
||||
Dictionary<string, FixedPoint2> rememberedReagents = new();
|
||||
|
||||
if (TryComp<MapGridComponent>(xform.GridUid, out var grid))
|
||||
{
|
||||
var tile = _map.GetTileRef(xform.GridUid.Value, grid, xform.Coordinates);
|
||||
|
||||
// Get liquid color and reagents from puddle or FloorWaterEntity
|
||||
if (_puddles.TryGetPuddle(tile, out var puddle) && TryComp(puddle, out PuddleComponent? puddleComp) && puddleComp.Solution != null)
|
||||
{
|
||||
var sol = puddleComp.Solution.Value.Comp.Solution;
|
||||
liquidColor = sol.GetColor(_protos);
|
||||
|
||||
// Remember all reagents in the solution
|
||||
foreach (var (reagentId, quantity) in sol.Contents)
|
||||
{
|
||||
rememberedReagents[reagentId.ToString()] = quantity;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// FloorWaterEntity fallback -> use Water reagent color
|
||||
liquidColor = _protos.Index<ReagentPrototype>("Water").SubstanceColor;
|
||||
rememberedReagents["Water"] = FixedPoint2.New(30); // Assume water
|
||||
}
|
||||
}
|
||||
|
||||
// Determine spawn prototype: mostly rainbow carp, rarely holo/dungeon
|
||||
string protoId = "MobCarpServantRainbow"; // Default to rainbow
|
||||
|
||||
if (egg.Queen != null && TryComp(egg.Queen.Value, out CarpQueenComponent? queen))
|
||||
{
|
||||
// Use spawn chances from queen component
|
||||
var roll = _rand.Next(100);
|
||||
var cumulative = 0;
|
||||
var selected = false;
|
||||
|
||||
foreach (var (proto, chance) in queen.SpawnChances)
|
||||
{
|
||||
cumulative += chance;
|
||||
if (roll < cumulative)
|
||||
{
|
||||
protoId = proto;
|
||||
selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no spawn chance matched (sum < 100), default to rainbow
|
||||
if (!selected)
|
||||
protoId = "MobCarpServantRainbow";
|
||||
}
|
||||
|
||||
var mob = Spawn(protoId, xform.Coordinates);
|
||||
|
||||
// Store liquid memory
|
||||
var memory = EnsureComp<CarpServantMemoryComponent>(mob);
|
||||
memory.LiquidColor = liquidColor;
|
||||
memory.RememberedReagents = rememberedReagents;
|
||||
|
||||
// Check if queen is nearby
|
||||
bool queenNearby = false;
|
||||
EntityUid? closestFriend = null;
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
if (egg.Queen != null && Exists(egg.Queen.Value))
|
||||
{
|
||||
var queenXform = Transform(egg.Queen.Value);
|
||||
var queenCoords = queenXform.Coordinates.ToMap(EntityManager, _xformSys);
|
||||
var mobCoords = xform.Coordinates.ToMap(EntityManager, _xformSys);
|
||||
var distance = (queenCoords.Position - mobCoords.Position).Length();
|
||||
|
||||
// Consider queen "nearby" if within configured range
|
||||
if (distance <= egg.QueenCheckRange)
|
||||
queenNearby = true;
|
||||
}
|
||||
|
||||
// Always remember nearby players (within configured range)
|
||||
var nearbyEntities = new HashSet<EntityUid>();
|
||||
_lookup.GetEntitiesInRange(xform.Coordinates, egg.FriendSearchRange, nearbyEntities);
|
||||
|
||||
var exception = EnsureComp<FactionExceptionComponent>(mob);
|
||||
|
||||
foreach (var entity in nearbyEntities)
|
||||
{
|
||||
// Check if it's a humanoid (same as MobTomatoKiller uses whitelist with HumanoidAppearanceComponent)
|
||||
// This will match both players and AI with humanoid appearance
|
||||
if (HasComp<HumanoidAppearanceComponent>(entity))
|
||||
{
|
||||
memory.RememberedFriends.Add(entity);
|
||||
|
||||
// Add to faction exceptions so they won't be attacked (unless queen orders)
|
||||
// Use NpcFactionSystem to properly add to ignored list
|
||||
if (!_npcFaction.IsIgnored((mob, exception), entity))
|
||||
{
|
||||
_npcFaction.IgnoreEntity((mob, exception), (entity, null));
|
||||
}
|
||||
|
||||
// Track closest friend for following
|
||||
var entityXform = Transform(entity);
|
||||
var entityCoords = entityXform.Coordinates.ToMap(EntityManager, _xformSys);
|
||||
var mobCoords = xform.Coordinates.ToMap(EntityManager, _xformSys);
|
||||
var friendDistance = (entityCoords.Position - mobCoords.Position).Length();
|
||||
|
||||
if (friendDistance < closestDistance)
|
||||
{
|
||||
closestDistance = friendDistance;
|
||||
closestFriend = entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If queen is nearby, make carp a servant; otherwise, let it work as normal carp
|
||||
if (queenNearby && egg.Queen != null && Exists(egg.Queen.Value))
|
||||
{
|
||||
// Make carp a servant of the queen
|
||||
if (TryComp(egg.Queen, out CarpQueenComponent? qc))
|
||||
{
|
||||
var queenUid = egg.Queen.Value;
|
||||
memory.RememberedFriends.Add(queenUid);
|
||||
if (!_npcFaction.IsIgnored((mob, exception), queenUid))
|
||||
{
|
||||
_npcFaction.IgnoreEntity((mob, exception), (queenUid, null));
|
||||
}
|
||||
|
||||
var comp = EnsureComp<CarpQueenServantComponent>(mob);
|
||||
comp.Queen = egg.Queen;
|
||||
Dirty(mob, comp);
|
||||
qc.Servants.Add(mob);
|
||||
// Remove egg from tracking
|
||||
qc.Eggs.Remove(uid);
|
||||
|
||||
// Follow queen and execute her commands
|
||||
_npc.SetBlackboard(mob, NPCBlackboard.FollowTarget, new EntityCoordinates(egg.Queen.Value, Vector2.Zero));
|
||||
_carpQueenSystem.UpdateServantNpc(mob, qc.CurrentOrder);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Queen is not nearby - carp works as normal carp (like MobTomatoKiller)
|
||||
// Remove servant components if they exist
|
||||
RemComp<CarpQueenServantComponent>(mob);
|
||||
|
||||
// Use normal carp HTN compound instead of RatServantCompound
|
||||
if (TryComp<HTNComponent>(mob, out var htn))
|
||||
{
|
||||
// Change HTN root task to normal carp behavior
|
||||
htn.RootTask = new HTNCompoundTask { Task = "DragonCarpCompound" };
|
||||
_htn.Replan(htn);
|
||||
}
|
||||
|
||||
// Set follow target to closest friend if available
|
||||
if (closestFriend != null)
|
||||
{
|
||||
_npc.SetBlackboard(mob, NPCBlackboard.FollowTarget, new EntityCoordinates(closestFriend.Value, Vector2.Zero));
|
||||
}
|
||||
|
||||
// Still remove egg from tracking if queen exists
|
||||
if (TryComp(egg.Queen, out CarpQueenComponent? qc))
|
||||
{
|
||||
qc.Eggs.Remove(uid);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply color to carp (will be handled by visualizer system)
|
||||
Dirty(mob, memory);
|
||||
QueueDel(uid);
|
||||
}
|
||||
|
||||
// Public entry-point for other systems (e.g. queen) to re-check hatching conditions
|
||||
public void RequestHatchCheck(EntityUid uid)
|
||||
{
|
||||
if (!TryComp(uid, out CarpEggComponent? egg))
|
||||
return;
|
||||
|
||||
TryHatchCheck(uid, egg);
|
||||
}
|
||||
|
||||
private void OnEggDestroyed(EntityUid uid, CarpEggComponent egg, DestructionEventArgs args)
|
||||
{
|
||||
// Spill 2u of a random reagent on destruction
|
||||
var reagents = _protos.EnumeratePrototypes<ReagentPrototype>();
|
||||
string chosen = null!;
|
||||
var count = 0;
|
||||
foreach (var r in reagents)
|
||||
{
|
||||
count++;
|
||||
if (_rand.Prob(1f / count))
|
||||
chosen = r.ID;
|
||||
}
|
||||
|
||||
if (chosen != null)
|
||||
{
|
||||
var sol = new Solution(chosen, FixedPoint2.New(2));
|
||||
_puddles.TrySpillAt(uid, sol, out _, sound: false);
|
||||
}
|
||||
|
||||
// Remove egg from queen tracking
|
||||
if (egg.Queen != null && TryComp(egg.Queen.Value, out CarpQueenComponent? queen))
|
||||
{
|
||||
queen.Eggs.Remove(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEggShutdown(EntityUid uid, CarpEggComponent egg, ComponentShutdown args)
|
||||
{
|
||||
if (egg.Queen != null && TryComp(egg.Queen.Value, out CarpQueenComponent? queen))
|
||||
{
|
||||
queen.Eggs.Remove(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
Content.Server/_Sunrise/CarpQueen/CarpQueenStrengthSystem.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Content.Shared._Sunrise.CarpQueen;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Movement.Components;
|
||||
|
||||
namespace Content.Server._Sunrise.CarpQueen;
|
||||
|
||||
/// <summary>
|
||||
/// System that increases Carp Queen's pushing strength when in combat mode.
|
||||
/// </summary>
|
||||
public sealed class CarpQueenStrengthSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CarpQueenComponent, CombatModeChangedEvent>(OnCombatModeChanged);
|
||||
}
|
||||
|
||||
private void OnCombatModeChanged(EntityUid uid, CarpQueenComponent component, ref CombatModeChangedEvent args)
|
||||
{
|
||||
if (!TryComp<MobCollisionComponent>(uid, out var mobCollision))
|
||||
return;
|
||||
|
||||
// Base strength for normal mode, boosted strength for combat mode
|
||||
const float baseStrength = 50f;
|
||||
const float combatStrength = 200f; // 4x stronger in combat mode
|
||||
|
||||
mobCollision.Strength = args.IsInCombatMode ? combatStrength : baseStrength;
|
||||
Dirty(uid, mobCollision);
|
||||
}
|
||||
}
|
||||
|
||||
237
Content.Server/_Sunrise/CarpQueen/CarpQueenSystem.cs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
using System.Numerics;
|
||||
using Content.Server.NPC;
|
||||
using Content.Server.NPC.HTN;
|
||||
using Content.Server.NPC.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared._Sunrise.CarpQueen;
|
||||
using Content.Shared.Pointing;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.RatKing;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.NPC.Components;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Damage;
|
||||
|
||||
namespace Content.Server._Sunrise.CarpQueen;
|
||||
|
||||
public sealed class CarpQueenSystem : SharedCarpQueenSystem
|
||||
{
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly HTNSystem _htn = default!;
|
||||
[Dependency] private readonly CarpEggSystem _carpEggs = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly HungerSystem _hunger = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CarpQueenComponent, CarpQueenSummonActionEvent>(OnSummon);
|
||||
SubscribeLocalEvent<CarpQueenComponent, AfterPointedAtEvent>(OnPointedAt);
|
||||
SubscribeLocalEvent<CarpQueenServantComponent, ComponentShutdown>(OnServantShutdown);
|
||||
}
|
||||
|
||||
protected override void OnStartup(EntityUid uid, CarpQueenComponent component, ComponentStartup args)
|
||||
{
|
||||
base.OnStartup(uid, component, args);
|
||||
|
||||
if (TryComp<HungerComponent>(uid, out var hunger))
|
||||
component.LastObservedHunger = _hunger.GetHunger(hunger);
|
||||
}
|
||||
|
||||
private void OnSummon(EntityUid uid, CarpQueenComponent component, CarpQueenSummonActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (component.ArmyMobSpawnOptions.Count == 0)
|
||||
return;
|
||||
|
||||
// Limit total eggs + servants to MaxArmySize. Prune invalid references first.
|
||||
var toRemoveServants = new List<EntityUid>();
|
||||
var aliveServants = 0;
|
||||
foreach (var s in component.Servants)
|
||||
{
|
||||
if (!Exists(s))
|
||||
{
|
||||
toRemoveServants.Add(s);
|
||||
continue;
|
||||
}
|
||||
if (TryComp<MobStateComponent>(s, out var mobState) && mobState.CurrentState == MobState.Dead)
|
||||
continue;
|
||||
aliveServants++;
|
||||
}
|
||||
foreach (var rem in toRemoveServants)
|
||||
component.Servants.Remove(rem);
|
||||
|
||||
var toRemoveEggs = new List<EntityUid>();
|
||||
foreach (var e in component.Eggs)
|
||||
{
|
||||
if (!Exists(e))
|
||||
toRemoveEggs.Add(e);
|
||||
}
|
||||
foreach (var rem in toRemoveEggs)
|
||||
component.Eggs.Remove(rem);
|
||||
|
||||
var eggsCount = component.Eggs.Count;
|
||||
if (aliveServants + eggsCount >= component.MaxArmySize)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("carp-queen-max-army", ("amount", component.MaxArmySize)), uid, uid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hunger cost like Rat King
|
||||
if (!TryComp<HungerComponent>(uid, out var hungerComp))
|
||||
return;
|
||||
|
||||
if (_hunger.GetHunger(hungerComp) < component.HungerPerSummon)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, uid);
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
_hunger.ModifyHunger(uid, -component.HungerPerSummon, hungerComp);
|
||||
// Spawn egg instead of immediate servant
|
||||
var egg = Spawn("MobCarpEgg", Transform(uid).Coordinates);
|
||||
var eggComp = EnsureComp<CarpEggComponent>(egg);
|
||||
eggComp.Queen = uid;
|
||||
Dirty(egg, eggComp);
|
||||
component.Eggs.Add(egg);
|
||||
|
||||
// Trigger hatch check now that queen is assigned (covers tiles already containing liquid/FloorWater)
|
||||
_carpEggs.RequestHatchCheck(egg);
|
||||
_popup.PopupEntity(Loc.GetString("carp-queen-summon-popup"), uid, uid);
|
||||
}
|
||||
|
||||
private void OnServantShutdown(EntityUid uid, CarpQueenServantComponent servant, ComponentShutdown args)
|
||||
{
|
||||
if (servant.Queen == null || !TryComp(servant.Queen.Value, out CarpQueenComponent? queen))
|
||||
return;
|
||||
|
||||
queen.Servants.Remove(uid);
|
||||
}
|
||||
|
||||
private void OnPointedAt(EntityUid uid, CarpQueenComponent component, ref AfterPointedAtEvent args)
|
||||
{
|
||||
if (component.CurrentOrder != CarpQueenOrderType.Kill)
|
||||
return;
|
||||
|
||||
var target = args.Pointed;
|
||||
if (!Exists(target))
|
||||
return;
|
||||
|
||||
// Accept any living mob (players or AI). Ignore objects.
|
||||
var valid = false;
|
||||
if (TryComp<MobStateComponent>(target, out var mobState))
|
||||
valid = mobState.CurrentState != MobState.Dead;
|
||||
else if (HasComp<NpcFactionMemberComponent>(target))
|
||||
valid = true;
|
||||
else if (HasComp<ActorComponent>(target))
|
||||
valid = true;
|
||||
|
||||
if (!valid)
|
||||
return;
|
||||
|
||||
foreach (var servant in component.Servants)
|
||||
{
|
||||
// Skip if servant is being deleted or doesn't exist
|
||||
if (TerminatingOrDeleted(servant))
|
||||
continue;
|
||||
|
||||
if (TryComp<CarpServantMemoryComponent>(servant, out var memory))
|
||||
{
|
||||
var exception = EnsureComp<FactionExceptionComponent>(servant);
|
||||
|
||||
if (_npcFaction.IsIgnored((servant, exception), target))
|
||||
_npcFaction.UnignoreEntity((servant, exception), target);
|
||||
|
||||
if (memory.RememberedFriends.Remove(target))
|
||||
Dirty(servant, memory);
|
||||
|
||||
if (memory.ForbiddenTargets.Remove(target))
|
||||
Dirty(servant, memory);
|
||||
}
|
||||
|
||||
_npc.SetBlackboard(servant, NPCBlackboard.CurrentOrderedTarget, target);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// Small self-heal when hunger increases (i.e., when eating).
|
||||
var query = EntityQueryEnumerator<CarpQueenComponent, HungerComponent>();
|
||||
while (query.MoveNext(out var uid, out var queen, out var hunger))
|
||||
{
|
||||
var current = _hunger.GetHunger(hunger);
|
||||
if (current > queen.LastObservedHunger)
|
||||
{
|
||||
var delta = current - queen.LastObservedHunger;
|
||||
var heal = MathF.Min(delta * queen.HealPerHunger, queen.MaxHealPerTick);
|
||||
if (heal > 0f)
|
||||
{
|
||||
var spec = new DamageSpecifier();
|
||||
spec.DamageDict["Blunt"] = -heal / 2f;
|
||||
spec.DamageDict["Slash"] = -heal / 2f;
|
||||
spec.DamageDict["Heat"] = 0f; // leave as 0; can be adjusted later
|
||||
_damageable.TryChangeDamage(uid, spec, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
queen.LastObservedHunger = current;
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateServantNpc(EntityUid uid, CarpQueenOrderType orderType)
|
||||
{
|
||||
base.UpdateServantNpc(uid, orderType);
|
||||
|
||||
// Only update if this is actually a servant (has CarpQueenServantComponent)
|
||||
if (!TryComp<CarpQueenServantComponent>(uid, out var servant) || servant.Queen == null || !Exists(servant.Queen.Value))
|
||||
return;
|
||||
|
||||
if (!TryComp<HTNComponent>(uid, out var htn))
|
||||
return;
|
||||
|
||||
if (htn.Plan != null)
|
||||
_htn.ShutdownPlan(htn);
|
||||
|
||||
// Servant always follows queen and executes her commands
|
||||
_npc.SetBlackboard(uid, NPCBlackboard.FollowTarget, new EntityCoordinates(servant.Queen.Value, Vector2.Zero));
|
||||
|
||||
// Configure order and follow distances as requested (close follow ~1 tile)
|
||||
// Convert CarpQueenOrderType to RatKingOrderType for HTN compatibility
|
||||
var ratKingOrder = SharedCarpQueenSystem.ConvertToRatKingOrder(orderType);
|
||||
_npc.SetBlackboard(uid, NPCBlackboard.CurrentOrders, ratKingOrder);
|
||||
_npc.SetBlackboard(uid, "FollowCloseRange", 1.0f);
|
||||
_npc.SetBlackboard(uid, "FollowRange", 1.5f);
|
||||
_htn.Replan(htn);
|
||||
}
|
||||
|
||||
|
||||
public override void DoCommandCallout(EntityUid uid, CarpQueenComponent component)
|
||||
{
|
||||
base.DoCommandCallout(uid, component);
|
||||
|
||||
if (!component.OrderCallouts.TryGetValue(component.CurrentOrder, out var datasetId) ||
|
||||
!PrototypeManager.TryIndex<LocalizedDatasetPrototype>(datasetId, out var datasetPrototype))
|
||||
return;
|
||||
|
||||
var msg = Random.Pick(datasetPrototype);
|
||||
_chat.TrySendInGameICMessage(uid, msg, InGameICChatType.Speak, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
Content.Server/_Sunrise/CarpQueen/CarpServantBiteSystem.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
using Content.Server.Body.Systems;
|
||||
using Content.Shared._Sunrise.CarpQueen;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._Sunrise.CarpQueen;
|
||||
|
||||
/// <summary>
|
||||
/// System that handles carp servant bite mechanics:
|
||||
/// Injects 1u of each remembered reagent from the liquid the carp hatched from.
|
||||
/// </summary>
|
||||
public sealed class CarpServantBiteSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protos = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CarpServantMemoryComponent, MeleeHitEvent>(OnMeleeHit);
|
||||
}
|
||||
|
||||
private void OnMeleeHit(EntityUid uid, CarpServantMemoryComponent memory, MeleeHitEvent args)
|
||||
{
|
||||
if (memory.RememberedReagents.Count == 0)
|
||||
return;
|
||||
|
||||
// Inject 1u of each remembered reagent into each hit target
|
||||
foreach (var target in args.HitEntities)
|
||||
{
|
||||
if (!TryComp<BloodstreamComponent>(target, out var bloodstream))
|
||||
continue;
|
||||
|
||||
// Create solution with configured amount of each remembered reagent
|
||||
var solution = new Solution();
|
||||
foreach (var (reagentId, _) in memory.RememberedReagents)
|
||||
{
|
||||
if (_protos.HasIndex<ReagentPrototype>(reagentId))
|
||||
{
|
||||
solution.AddReagent(reagentId, memory.BiteReagentAmount);
|
||||
}
|
||||
}
|
||||
|
||||
if (solution.Volume > FixedPoint2.Zero)
|
||||
{
|
||||
_bloodstream.TryAddToChemicals((target, bloodstream), solution);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
103
Content.Server/_Sunrise/CarpQueen/CarpServantDisciplineSystem.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
using Content.Server.NPC;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Server.NPC.HTN;
|
||||
using Content.Server.NPC.Systems;
|
||||
using Content.Shared._Sunrise.CarpQueen;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.NPC.Components;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
|
||||
namespace Content.Server._Sunrise.CarpQueen;
|
||||
|
||||
/// <summary>
|
||||
/// System that handles discipline for tamed carps (hand-raised carps).
|
||||
/// If the owner hits their tamed carp with bare hands or gloves, the carp stops attacking its current target.
|
||||
/// The carp will resume attacking that target if the attacker damages the owner.
|
||||
/// </summary>
|
||||
public sealed class CarpServantDisciplineSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CarpServantMemoryComponent, AttackedEvent>(OnCarpAttacked);
|
||||
// Note: We don't subscribe to DamageChangedEvent here to avoid duplicate with CarpServantRetaliationSystem
|
||||
// The discipline logic (removing from forbidden targets) is handled separately
|
||||
}
|
||||
|
||||
private void OnCarpAttacked(EntityUid uid, CarpServantMemoryComponent memory, AttackedEvent args)
|
||||
{
|
||||
// Only handle if this is a tamed carp (not a queen's servant)
|
||||
if (HasComp<CarpQueenServantComponent>(uid))
|
||||
return;
|
||||
|
||||
// Check if attacker is one of the remembered friends (owner)
|
||||
if (!memory.RememberedFriends.Contains(args.User))
|
||||
return;
|
||||
|
||||
// Check if attack was made with bare hands or something in gloves slot
|
||||
// Used == User means no weapon (bare hands)
|
||||
// Or Used is an item equipped in the gloves slot of the user
|
||||
var isBareHands = args.Used == args.User;
|
||||
var isGlovesSlotItem = _inventory.TryGetSlotEntity(args.User, "gloves", out var glovesSlotItem) && glovesSlotItem == args.Used;
|
||||
|
||||
if (!isBareHands && !isGlovesSlotItem)
|
||||
return;
|
||||
|
||||
// Get current attack target from NPCMeleeCombatComponent or blackboard
|
||||
EntityUid? currentTarget = null;
|
||||
NPCMeleeCombatComponent? meleeCombat = null;
|
||||
HTNComponent? htn = null;
|
||||
|
||||
// Try to get from NPCMeleeCombatComponent first (most direct)
|
||||
if (TryComp<NPCMeleeCombatComponent>(uid, out meleeCombat) && meleeCombat.Target != EntityUid.Invalid)
|
||||
{
|
||||
currentTarget = meleeCombat.Target;
|
||||
}
|
||||
// Fallback to blackboard - check common target keys
|
||||
else if (TryComp<HTNComponent>(uid, out htn))
|
||||
{
|
||||
// Try CurrentOrderedTarget first (set by queen/orders)
|
||||
if (htn.Blackboard.TryGetValue<EntityUid>(NPCBlackboard.CurrentOrderedTarget, out var orderedTarget, EntityManager) && orderedTarget != EntityUid.Invalid)
|
||||
currentTarget = orderedTarget;
|
||||
// Then try generic "Target" key
|
||||
else if (htn.Blackboard.TryGetValue<EntityUid>("Target", out var target, EntityManager) && target != EntityUid.Invalid)
|
||||
currentTarget = target;
|
||||
}
|
||||
|
||||
// If there's a current target, add it to forbidden targets
|
||||
if (currentTarget != null && currentTarget != args.User)
|
||||
{
|
||||
memory.ForbiddenTargets.Add(currentTarget.Value);
|
||||
Dirty(uid, memory);
|
||||
|
||||
// Add to faction exceptions to prevent attack
|
||||
var exception = EnsureComp<FactionExceptionComponent>(uid);
|
||||
if (!_npcFaction.IsIgnored((uid, exception), currentTarget.Value))
|
||||
{
|
||||
_npcFaction.IgnoreEntity((uid, exception), (currentTarget.Value, null));
|
||||
}
|
||||
|
||||
// Clear the attack target from blackboard/combat component
|
||||
if (meleeCombat != null)
|
||||
{
|
||||
meleeCombat.Target = EntityUid.Invalid;
|
||||
}
|
||||
else if (htn != null)
|
||||
{
|
||||
// Clear target keys from blackboard
|
||||
if (htn.Blackboard.ContainsKey("Target"))
|
||||
_npc.SetBlackboard(uid, "Target", EntityUid.Invalid);
|
||||
if (htn.Blackboard.ContainsKey(NPCBlackboard.CurrentOrderedTarget))
|
||||
_npc.SetBlackboard(uid, NPCBlackboard.CurrentOrderedTarget, EntityUid.Invalid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
using Content.Shared._Sunrise.CarpQueen;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.NPC.Components;
|
||||
using Content.Shared.NPC.Systems;
|
||||
|
||||
namespace Content.Server._Sunrise.CarpQueen;
|
||||
|
||||
/// <summary>
|
||||
/// System that makes tamed carps retaliate against entities that damage their remembered friends.
|
||||
/// </summary>
|
||||
public sealed class CarpServantRetaliationSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DamageableComponent, DamageChangedEvent>(OnDamageChanged);
|
||||
}
|
||||
|
||||
private void OnDamageChanged(EntityUid uid, DamageableComponent component, DamageChangedEvent args)
|
||||
{
|
||||
// Only react to damage increases
|
||||
if (!args.DamageIncreased)
|
||||
return;
|
||||
|
||||
// Get the damaged entity (uid is the entity that received damage)
|
||||
var damagedEntity = uid;
|
||||
|
||||
// Get the attacker
|
||||
if (args.Origin is not { } attacker)
|
||||
return;
|
||||
|
||||
// Don't retaliate against inanimate objects
|
||||
if (!HasComp<MobStateComponent>(attacker))
|
||||
return;
|
||||
|
||||
// Find all carps that remember the damaged entity as a friend
|
||||
var query = EntityQueryEnumerator<CarpServantMemoryComponent>();
|
||||
while (query.MoveNext(out var carpUid, out var memory))
|
||||
{
|
||||
// Skip if carp is being deleted
|
||||
if (TerminatingOrDeleted(carpUid))
|
||||
continue;
|
||||
|
||||
// Check if the damaged entity is one of this carp's remembered friends
|
||||
if (!memory.RememberedFriends.Contains(damagedEntity))
|
||||
continue;
|
||||
|
||||
var attackerIsFriend = memory.RememberedFriends.Contains(attacker);
|
||||
var isServant = TryComp<CarpQueenServantComponent>(carpUid, out var servant) && servant.Queen != null;
|
||||
|
||||
if (isServant)
|
||||
{
|
||||
// Servants only retaliate when the queen is harmed
|
||||
if (servant!.Queen != damagedEntity)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Free-roaming carp do not retaliate against other remembered friends
|
||||
if (attackerIsFriend)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Aggro on the attacker
|
||||
var exception = EnsureComp<FactionExceptionComponent>(carpUid);
|
||||
|
||||
// Also handle discipline logic: if attacker was in forbidden targets, remove it
|
||||
// This allows the carp to attack again after the attacker damaged the owner
|
||||
if (memory.ForbiddenTargets.Remove(attacker))
|
||||
{
|
||||
Dirty(carpUid, memory);
|
||||
}
|
||||
|
||||
// Unignore and aggro the attacker (allows attack after discipline was cleared)
|
||||
_npcFaction.UnignoreEntity((carpUid, exception), attacker);
|
||||
_npcFaction.AggroEntity((carpUid, exception), (attacker, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +87,11 @@ public abstract class SharedCombatModeSystem : EntitySystem
|
|||
component.IsInCombatMode = value;
|
||||
Dirty(entity, component);
|
||||
|
||||
// Sunrise-Start
|
||||
var ev = new CombatModeChangedEvent(value);
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
// Sunrise-End
|
||||
|
||||
if (component.CombatToggleActionEntity != null)
|
||||
_actionsSystem.SetToggled(component.CombatToggleActionEntity, component.IsInCombatMode);
|
||||
|
||||
|
|
@ -129,3 +134,9 @@ public sealed partial class ToggleCombatActionEvent : InstantActionEvent
|
|||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when combat mode changes for an entity.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct CombatModeChangedEvent(bool IsInCombatMode);
|
||||
|
|
|
|||
|
|
@ -120,6 +120,23 @@ public sealed partial class NpcFactionSystem
|
|||
tracker.Entities.Remove(ent);
|
||||
}
|
||||
|
||||
// Sunrise-Start
|
||||
/// <summary>
|
||||
/// Makes an entity no longer be ignored, if it was.
|
||||
/// Allows the NPC to attack the entity again.
|
||||
/// </summary>
|
||||
public void UnignoreEntity(Entity<FactionExceptionComponent?> ent, EntityUid target)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
if (!ent.Comp.Ignored.Remove(target) || !_trackerQuery.TryGetComponent(target, out var tracker))
|
||||
return;
|
||||
|
||||
tracker.Entities.Remove(ent);
|
||||
}
|
||||
// Sunrise-End
|
||||
|
||||
/// <summary>
|
||||
/// Makes a list of entities no longer be considered hostile, if it was.
|
||||
/// Doesn't apply to regular faction hostilities.
|
||||
|
|
|
|||
54
Content.Shared/_Sunrise/CarpQueen/CarpEggComponent.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared._Sunrise.CarpQueen;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class CarpEggComponent : Component
|
||||
{
|
||||
[DataField("queen")] public EntityUid? Queen;
|
||||
|
||||
/// <summary>
|
||||
/// Required puddle volume (u) to hatch.
|
||||
/// </summary>
|
||||
[DataField("requiredVolume")] public float RequiredVolume = 15f;
|
||||
|
||||
/// <summary>
|
||||
/// Seconds between hatch checks.
|
||||
/// </summary>
|
||||
[DataField("checkInterval")] public float CheckInterval = 3f;
|
||||
|
||||
[DataField("accum")] public float Accum;
|
||||
|
||||
/// <summary>
|
||||
/// Seconds the egg must remain on valid liquid before hatching.
|
||||
/// </summary>
|
||||
[DataField("hatchDelay")] public float HatchDelay = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether current tile conditions are sufficient for hatching.
|
||||
/// </summary>
|
||||
[DataField("eligible")] public bool Eligible;
|
||||
|
||||
/// <summary>
|
||||
/// Accumulated time spent waiting without valid liquid. If exceeds MaxWaitWithoutLiquid, egg breaks.
|
||||
/// </summary>
|
||||
[DataField("waitElapsed")] public float WaitElapsed;
|
||||
|
||||
/// <summary>
|
||||
/// Max seconds to wait for liquid to appear before breaking the egg.
|
||||
/// </summary>
|
||||
[DataField("maxWaitWithoutLiquid")] public float MaxWaitWithoutLiquid = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// Range (in tiles) to check if queen is nearby when hatching.
|
||||
/// If queen is within this range, carp becomes servant; otherwise, it imprints on nearby players.
|
||||
/// </summary>
|
||||
[DataField("queenCheckRange")] public float QueenCheckRange = 3f;
|
||||
|
||||
/// <summary>
|
||||
/// Range (in tiles) to search for nearby players to imprint on when queen is not nearby.
|
||||
/// </summary>
|
||||
[DataField("friendSearchRange")] public float FriendSearchRange = 3f;
|
||||
}
|
||||
|
||||
|
||||
11
Content.Shared/_Sunrise/CarpQueen/CarpEggVisuals.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.CarpQueen;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum CarpEggVisuals
|
||||
{
|
||||
OverlayColor
|
||||
}
|
||||
|
||||
|
||||
11
Content.Shared/_Sunrise/CarpQueen/CarpQueenAccessSystem.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace Content.Shared._Sunrise.CarpQueen;
|
||||
|
||||
/// <summary>
|
||||
/// Marker system that grants access permissions to mutate Carp Queen components from server systems.
|
||||
/// Server systems that need write access should inherit from this.
|
||||
/// </summary>
|
||||
public abstract class CarpQueenAccessSystem : EntitySystem
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
31
Content.Shared/_Sunrise/CarpQueen/CarpQueenActionEvents.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.CarpQueen;
|
||||
|
||||
public sealed partial class CarpQueenSummonActionEvent : InstantActionEvent
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event for carp queen order actions (Stay, Follow, Kill, Loose).
|
||||
/// </summary>
|
||||
public sealed partial class CarpQueenOrderActionEvent : InstantActionEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of order being given
|
||||
/// </summary>
|
||||
[DataField("type")]
|
||||
public CarpQueenOrderType Type;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum CarpQueenOrderType : byte
|
||||
{
|
||||
Stay,
|
||||
Follow,
|
||||
Kill,
|
||||
Loose
|
||||
}
|
||||
|
||||
|
||||
130
Content.Shared/_Sunrise/CarpQueen/CarpQueenComponent.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared._Sunrise.CarpQueen;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCarpQueenSystem), typeof(CarpQueenAccessSystem))]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class CarpQueenComponent : Component
|
||||
{
|
||||
[DataField("actionSummon", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string ActionSummon = "ActionCarpQueenSummon";
|
||||
|
||||
[DataField("actionSummonEntity")]
|
||||
public EntityUid? ActionSummonEntity;
|
||||
|
||||
[DataField("actionOrderStay", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string ActionOrderStay = "ActionCarpQueenOrderStay";
|
||||
|
||||
[DataField("actionOrderStayEntity")]
|
||||
public EntityUid? ActionOrderStayEntity;
|
||||
|
||||
[DataField("actionOrderFollow", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string ActionOrderFollow = "ActionCarpQueenOrderFollow";
|
||||
|
||||
[DataField("actionOrderFollowEntity")]
|
||||
public EntityUid? ActionOrderFollowEntity;
|
||||
|
||||
[DataField("actionOrderKill", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string ActionOrderKill = "ActionCarpQueenOrderKill";
|
||||
|
||||
[DataField("actionOrderKillEntity")]
|
||||
public EntityUid? ActionOrderKillEntity;
|
||||
|
||||
[DataField("actionOrderLoose", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string ActionOrderLoose = "ActionCarpQueenOrderLoose";
|
||||
|
||||
[DataField("actionOrderLooseEntity")]
|
||||
public EntityUid? ActionOrderLooseEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Current order applied to servants.
|
||||
/// </summary>
|
||||
[DataField("currentOrders"), AutoNetworkedField]
|
||||
public CarpQueenOrderType CurrentOrder = CarpQueenOrderType.Loose;
|
||||
|
||||
/// <summary>
|
||||
/// List of spawned servants controlled by the queen.
|
||||
/// </summary>
|
||||
[DataField("servants")]
|
||||
public HashSet<EntityUid> Servants = new();
|
||||
|
||||
/// <summary>
|
||||
/// Active eggs spawned by the queen (not yet hatched).
|
||||
/// </summary>
|
||||
[DataField("eggs")]
|
||||
public HashSet<EntityUid> Eggs = new();
|
||||
|
||||
/// <summary>
|
||||
/// Pool of carp servant prototype IDs to randomly pick from when summoning.
|
||||
/// </summary>
|
||||
[DataField("armyMobSpawnOptions")]
|
||||
public List<string> ArmyMobSpawnOptions = new()
|
||||
{
|
||||
"MobCarpServantDungeon",
|
||||
"MobCarpServantMagic",
|
||||
"MobCarpServantHolo",
|
||||
"MobCarpServantRainbow",
|
||||
"MobCarpServantDragon"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Dataset mapping for order callouts (spoken lines on order change).
|
||||
/// </summary>
|
||||
[DataField("orderCallouts")]
|
||||
public Dictionary<CarpQueenOrderType, string> OrderCallouts = new()
|
||||
{
|
||||
{ CarpQueenOrderType.Stay, "CarpQueenCommandStay" },
|
||||
{ CarpQueenOrderType.Follow, "CarpQueenCommandFollow" },
|
||||
{ CarpQueenOrderType.Kill, "CarpQueenCommandKill" },
|
||||
{ CarpQueenOrderType.Loose, "CarpQueenCommandLoose" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Hunger consumed per summon use.
|
||||
/// </summary>
|
||||
[DataField("hungerPerSummon")]
|
||||
public float HungerPerSummon = 25f;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks last observed hunger to grant small healing when eating.
|
||||
/// Server-side only; not networked.
|
||||
/// </summary>
|
||||
public float LastObservedHunger;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum total servants + eggs the queen can have at once.
|
||||
/// </summary>
|
||||
[DataField("maxArmySize")]
|
||||
public int MaxArmySize = 5;
|
||||
|
||||
/// <summary>
|
||||
/// HP healed per 1 unit of hunger gained (when eating).
|
||||
/// </summary>
|
||||
[DataField("healPerHunger")]
|
||||
public float HealPerHunger = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum HP healed per tick from eating.
|
||||
/// </summary>
|
||||
[DataField("maxHealPerTick")]
|
||||
public float MaxHealPerTick = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Spawn chances for different carp types when hatching from egg.
|
||||
/// Key: prototype ID, Value: chance (0-100).
|
||||
/// If sum is less than 100, remaining chance goes to default (MobCarpServantRainbow).
|
||||
/// </summary>
|
||||
[DataField("spawnChances")]
|
||||
public Dictionary<string, int> SpawnChances = new()
|
||||
{
|
||||
{ "MobCarpServantRainbow", 80 },
|
||||
{ "MobCarpServantHolo", 10 },
|
||||
{ "MobCarpServantDungeon", 10 }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared._Sunrise.CarpQueen;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCarpQueenSystem), typeof(CarpQueenAccessSystem))]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class CarpQueenServantComponent : Component
|
||||
{
|
||||
[DataField("queen"), AutoNetworkedField]
|
||||
public EntityUid? Queen;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared._Sunrise.CarpQueen;
|
||||
|
||||
/// <summary>
|
||||
/// Component that stores memory of the liquid the carp hatched from,
|
||||
/// including its color and reagents for injection on bite.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class CarpServantMemoryComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Color of the liquid the carp hatched from.
|
||||
/// Used for visual appearance.
|
||||
/// </summary>
|
||||
[DataField("liquidColor"), AutoNetworkedField]
|
||||
public Color LiquidColor = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary of reagent IDs and their amounts that were in the liquid.
|
||||
/// Used for injection on bite.
|
||||
/// </summary>
|
||||
[DataField("rememberedReagents"), AutoNetworkedField]
|
||||
public Dictionary<string, FixedPoint2> RememberedReagents = new();
|
||||
|
||||
/// <summary>
|
||||
/// Amount of each remembered reagent to inject per bite (in units).
|
||||
/// </summary>
|
||||
[DataField("biteReagentAmount")]
|
||||
public FixedPoint2 BiteReagentAmount = FixedPoint2.New(1);
|
||||
|
||||
/// <summary>
|
||||
/// List of players that were nearby when the carp hatched.
|
||||
/// These players are considered "friends" and won't be attacked
|
||||
/// unless the queen orders it.
|
||||
/// </summary>
|
||||
[DataField("rememberedFriends"), AutoNetworkedField]
|
||||
public HashSet<EntityUid> RememberedFriends = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of entities that the carp is temporarily forbidden to attack.
|
||||
/// These are cleared when the attacker damages the carp's owner.
|
||||
/// </summary>
|
||||
[DataField("forbiddenTargets")]
|
||||
public HashSet<EntityUid> ForbiddenTargets = new();
|
||||
}
|
||||
|
||||
114
Content.Shared/_Sunrise/CarpQueen/SharedCarpQueenSystem.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.RatKing;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Shared._Sunrise.CarpQueen;
|
||||
|
||||
public abstract class SharedCarpQueenSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
||||
[Dependency] protected readonly IRobustRandom Random = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<CarpQueenComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<CarpQueenComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<CarpQueenComponent, CarpQueenOrderActionEvent>(OnOrderAction);
|
||||
}
|
||||
|
||||
protected virtual void OnStartup(EntityUid uid, CarpQueenComponent component, ComponentStartup args)
|
||||
{
|
||||
if (!TryComp(uid, out ActionsComponent? comp))
|
||||
return;
|
||||
|
||||
_actions.AddAction(uid, ref component.ActionSummonEntity, component.ActionSummon, component: comp);
|
||||
_actions.AddAction(uid, ref component.ActionOrderStayEntity, component.ActionOrderStay, component: comp);
|
||||
_actions.AddAction(uid, ref component.ActionOrderFollowEntity, component.ActionOrderFollow, component: comp);
|
||||
_actions.AddAction(uid, ref component.ActionOrderKillEntity, component.ActionOrderKill, component: comp);
|
||||
_actions.AddAction(uid, ref component.ActionOrderLooseEntity, component.ActionOrderLoose, component: comp);
|
||||
|
||||
UpdateActions(uid, component);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, CarpQueenComponent component, ComponentShutdown args)
|
||||
{
|
||||
foreach (var servant in component.Servants)
|
||||
{
|
||||
if (TryComp(servant, out CarpQueenServantComponent? servantComp))
|
||||
servantComp.Queen = null;
|
||||
}
|
||||
|
||||
if (!TryComp(uid, out ActionsComponent? comp))
|
||||
return;
|
||||
|
||||
var actions = new Entity<ActionsComponent?>(uid, comp);
|
||||
_actions.RemoveAction(actions, component.ActionSummonEntity);
|
||||
_actions.RemoveAction(actions, component.ActionOrderStayEntity);
|
||||
_actions.RemoveAction(actions, component.ActionOrderFollowEntity);
|
||||
_actions.RemoveAction(actions, component.ActionOrderKillEntity);
|
||||
_actions.RemoveAction(actions, component.ActionOrderLooseEntity);
|
||||
}
|
||||
|
||||
private void OnOrderAction(EntityUid uid, CarpQueenComponent component, CarpQueenOrderActionEvent args)
|
||||
{
|
||||
if (component.CurrentOrder == args.Type)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
component.CurrentOrder = args.Type;
|
||||
Dirty(uid, component);
|
||||
|
||||
UpdateActions(uid, component);
|
||||
UpdateAllServants(uid, component);
|
||||
DoCommandCallout(uid, component);
|
||||
}
|
||||
|
||||
private void UpdateActions(EntityUid uid, CarpQueenComponent component)
|
||||
{
|
||||
_actions.SetToggled(component.ActionOrderStayEntity, component.CurrentOrder == CarpQueenOrderType.Stay);
|
||||
_actions.SetToggled(component.ActionOrderFollowEntity, component.CurrentOrder == CarpQueenOrderType.Follow);
|
||||
_actions.SetToggled(component.ActionOrderKillEntity, component.CurrentOrder == CarpQueenOrderType.Kill);
|
||||
_actions.SetToggled(component.ActionOrderLooseEntity, component.CurrentOrder == CarpQueenOrderType.Loose);
|
||||
_actions.StartUseDelay(component.ActionOrderStayEntity);
|
||||
_actions.StartUseDelay(component.ActionOrderFollowEntity);
|
||||
_actions.StartUseDelay(component.ActionOrderKillEntity);
|
||||
_actions.StartUseDelay(component.ActionOrderLooseEntity);
|
||||
}
|
||||
|
||||
public void UpdateAllServants(EntityUid uid, CarpQueenComponent component)
|
||||
{
|
||||
foreach (var servant in component.Servants)
|
||||
{
|
||||
UpdateServantNpc(servant, component.CurrentOrder);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void UpdateServantNpc(EntityUid uid, CarpQueenOrderType orderType)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void DoCommandCallout(EntityUid uid, CarpQueenComponent component)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts CarpQueenOrderType to RatKingOrderType for HTN compatibility.
|
||||
/// HTN compounds use RatKingOrderType, so we need to map our order types to them.
|
||||
/// </summary>
|
||||
public static RatKingOrderType ConvertToRatKingOrder(CarpQueenOrderType orderType)
|
||||
{
|
||||
return orderType switch
|
||||
{
|
||||
CarpQueenOrderType.Stay => RatKingOrderType.Stay,
|
||||
CarpQueenOrderType.Follow => RatKingOrderType.Follow,
|
||||
CarpQueenOrderType.Kill => RatKingOrderType.CheeseEm,
|
||||
CarpQueenOrderType.Loose => RatKingOrderType.Loose,
|
||||
_ => RatKingOrderType.Loose
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
ent-ActionCarpQueenSummon = Lay roe
|
||||
.desc = Lays eggs that hatch into a random carp when exposed to liquid.
|
||||
ent-ActionCarpQueenOrderStay = Rest
|
||||
.desc = Commands the servant carps to ignore threats.
|
||||
ent-ActionCarpQueenOrderFollow = Follow
|
||||
.desc = Commands the servant carps to follow you closely.
|
||||
ent-ActionCarpQueenOrderKill = Kill it
|
||||
.desc = Commands the servant carps to attack the target you indicate.
|
||||
ent-ActionCarpQueenOrderLoose = Free
|
||||
.desc = Commands the servant carps to act according to the will of Mother Nature.
|
||||
|
||||
carp-queen-summon-popup = You have laid roe!
|
||||
|
||||
|
||||
|
|
@ -3,3 +3,6 @@ ent-SpawnPointGhostFoliant = ghost role spawn point
|
|||
.desc = { ent-MarkerBase.desc }
|
||||
ent-SpawnPointGhostTerminator = terminator spawn point
|
||||
.desc = { ent-MarkerBase.desc }
|
||||
ent-SpawnPointGhostCarpQueen = { ent-SpawnPointGhostFoliant }
|
||||
.suffix = carp queen
|
||||
.desc = { ent-MarkerBase.desc }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
ent-MobCarpQueen = carp queen
|
||||
.desc = The sovereign of the shoal.
|
||||
.suffix = { ent-MobHellspawnGhostRole.suffix }
|
||||
|
||||
ghost-role-information-carp-queen-name = Carp Queen
|
||||
ghost-role-information-carp-queen-description = Command your school of carps, hatching them from roes in the water, and make them do everything your majesty commands.
|
||||
|
||||
carp-egg-activates = Roe is starting to throb!
|
||||
carp-queen-max-army = Maximum { $amount } servants and roes combined.
|
||||
|
|
@ -1,2 +1,5 @@
|
|||
ent-EggSpiderFertilized = egg spider
|
||||
.desc = Is it a gemstone? Is it an egg? It looks expensive.
|
||||
|
||||
ent-MobCarpEgg = carp egg
|
||||
.desc = A pulsing mass, waiting to hatch.
|
||||
|
|
|
|||
13
Resources/Locale/en-US/datasets/carp_queen_commands.ftl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
carp-queen-command-stay-1 = Grr...
|
||||
carp-queen-command-stay-2 = Grrrr...
|
||||
carp-queen-command-stay-3 = Grk... grr...
|
||||
carp-queen-command-follow-1 = Grr! Grr!
|
||||
carp-queen-command-follow-2 = Grr-rr... grr.
|
||||
carp-queen-command-kill-1 = GRR!
|
||||
carp-queen-command-kill-2 = Grr-RAH!
|
||||
carp-queen-command-kill-3 = Grrr... RAH!
|
||||
carp-queen-command-kill-4 = GRRRRR!
|
||||
carp-queen-command-loose-1 = Grrr...
|
||||
carp-queen-command-loose-2 = Gr-rr...
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
ent-ActionCarpQueenSummon = Отложить икру
|
||||
.desc = Откладывает икру, которая при попадании в жидкость вылупляется в случайного карпа.
|
||||
ent-ActionCarpQueenOrderStay = Отдых
|
||||
.desc = Приказывает карпам-слугам игнорировать угрозы.
|
||||
ent-ActionCarpQueenOrderFollow = Следуй
|
||||
.desc = Приказывает карпам-слугам следовать вплотную за вами.
|
||||
ent-ActionCarpQueenOrderKill = Убейте его
|
||||
.desc = Приказывает карпам-слугам атаковать того, на кого вы укажете.
|
||||
ent-ActionCarpQueenOrderLoose = Вольно
|
||||
.desc = Приказывает карпам-слугам следовать по велению матушки-природы.
|
||||
|
||||
carp-queen-summon-popup = Вы отложили икру!
|
||||
|
|
@ -3,3 +3,6 @@ ent-SpawnPointGhostFoliant = точка спавна призрачной рол
|
|||
.desc = { ent-MarkerBase.desc }
|
||||
ent-SpawnPointGhostTerminator = точка спавна терминатора
|
||||
.desc = { ent-MarkerBase.desc }
|
||||
ent-SpawnPointGhostCarpQueen = { ent-SpawnPointGhostFoliant }
|
||||
.suffix = королева карпов
|
||||
.desc = { ent-MarkerBase.desc }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
ent-MobCarpQueen = королева карпов
|
||||
.desc = Повелительница стаи карпов.
|
||||
.suffix = { ent-MobHellspawnGhostRole.suffix }
|
||||
|
||||
ghost-role-information-carp-queen-name = Королева карпов
|
||||
ghost-role-information-carp-queen-description = Управляйте своей стаей карпов, вырастив их из икринок в воде, и заставляйте выполнять всё, что велит ваше величество.
|
||||
|
||||
carp-egg-activates = Икра начинает пульсировать!
|
||||
carp-queen-max-army = Максимум { $amount } слуг и икринок вместе.
|
||||
|
|
@ -1,2 +1,5 @@
|
|||
ent-EggSpiderFertilized = яйцо паука
|
||||
.desc = Это драгоценный камень? Или яйцо? Выглядит дорого.
|
||||
|
||||
ent-MobCarpEgg = икра карпа
|
||||
.desc = Пульсирующая масса, ожидающая вылупления.
|
||||
|
|
|
|||
13
Resources/Locale/ru-RU/datasets/carp_queen_commands.ftl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
carp-queen-command-stay-1 = Ррр...
|
||||
carp-queen-command-stay-2 = Гррр...
|
||||
carp-queen-command-stay-3 = Ррк... грр...
|
||||
carp-queen-command-follow-1 = ГРР! ГРР!
|
||||
carp-queen-command-follow-2 = Грр-рр... грр.
|
||||
carp-queen-command-kill-1 = ГРР!
|
||||
carp-queen-command-kill-2 = Грр-РА!
|
||||
carp-queen-command-kill-3 = Гррр... РА!
|
||||
carp-queen-command-kill-4 = ГРРРРР!
|
||||
carp-queen-command-loose-1 = Гррр...
|
||||
carp-queen-command-loose-2 = Гр-рр...
|
||||
|
||||
|
||||
91
Resources/Prototypes/_Sunrise/Actions/carp_queen.yml
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
- type: entity
|
||||
parent: BaseAction
|
||||
id: ActionCarpQueenSummon
|
||||
name: carp-queen-action-summon
|
||||
description: carp-queen-action-summon-desc
|
||||
components:
|
||||
- type: Action
|
||||
useDelay: 3
|
||||
icon:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: carpQueenEgg
|
||||
- type: InstantAction
|
||||
event: !type:CarpQueenSummonActionEvent
|
||||
|
||||
- type: entity
|
||||
parent: BaseAction
|
||||
id: ActionCarpQueenOrderStay
|
||||
name: carp-queen-action-stay
|
||||
description: carp-queen-action-stay-desc
|
||||
components:
|
||||
- type: Action
|
||||
useDelay: 1
|
||||
icon:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: stayOff
|
||||
iconOn:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: stay
|
||||
priority: 5
|
||||
- type: InstantAction
|
||||
event: !type:CarpQueenOrderActionEvent
|
||||
type: Stay
|
||||
|
||||
- type: entity
|
||||
parent: BaseAction
|
||||
id: ActionCarpQueenOrderFollow
|
||||
name: carp-queen-action-follow
|
||||
description: carp-queen-action-follow-desc
|
||||
components:
|
||||
- type: Action
|
||||
useDelay: 1
|
||||
icon:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: followOff
|
||||
iconOn:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: follow
|
||||
priority: 6
|
||||
- type: InstantAction
|
||||
event: !type:CarpQueenOrderActionEvent
|
||||
type: Follow
|
||||
|
||||
- type: entity
|
||||
parent: BaseAction
|
||||
id: ActionCarpQueenOrderKill
|
||||
name: carp-queen-action-kill
|
||||
description: carp-queen-action-kill-desc
|
||||
components:
|
||||
- type: Action
|
||||
useDelay: 1
|
||||
icon:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: attackOff
|
||||
iconOn:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: attack
|
||||
priority: 7
|
||||
- type: InstantAction
|
||||
event: !type:CarpQueenOrderActionEvent
|
||||
type: Kill
|
||||
|
||||
- type: entity
|
||||
parent: BaseAction
|
||||
id: ActionCarpQueenOrderLoose
|
||||
name: carp-queen-action-loose
|
||||
description: carp-queen-action-loose-desc
|
||||
components:
|
||||
- type: Action
|
||||
useDelay: 1
|
||||
icon:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: looseOff
|
||||
iconOn:
|
||||
sprite: _Sunrise/Interface/Actions/actions_carp_queen.rsi
|
||||
state: loose
|
||||
priority: 8
|
||||
- type: InstantAction
|
||||
event: !type:CarpQueenOrderActionEvent
|
||||
type: Loose
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
- type: localizedDataset
|
||||
id: CarpQueenCommandStay
|
||||
values:
|
||||
prefix: carp-queen-command-stay-
|
||||
count: 3
|
||||
|
||||
- type: localizedDataset
|
||||
id: CarpQueenCommandFollow
|
||||
values:
|
||||
prefix: carp-queen-command-follow-
|
||||
count: 2
|
||||
|
||||
- type: localizedDataset
|
||||
id: CarpQueenCommandKill
|
||||
values:
|
||||
prefix: carp-queen-command-kill-
|
||||
count: 4
|
||||
|
||||
- type: localizedDataset
|
||||
id: CarpQueenCommandLoose
|
||||
values:
|
||||
prefix: carp-queen-command-loose-
|
||||
count: 2
|
||||
|
||||
|
||||
|
|
@ -45,3 +45,25 @@
|
|||
- state: green
|
||||
- sprite: Mobs/Species/Terminator/parts.rsi
|
||||
state: full
|
||||
|
||||
- type: entity
|
||||
id: SpawnPointGhostCarpQueen
|
||||
parent: SpawnPointGhostFoliant
|
||||
suffix: carp queen
|
||||
components:
|
||||
- type: GhostRole
|
||||
name: ghost-role-information-carp-queen-name
|
||||
description: ghost-role-information-carp-queen-description
|
||||
rules: ghost-role-information-freeagent-rules
|
||||
mindRoles:
|
||||
- MindRoleGhostRoleSoloAntagonist
|
||||
raffle:
|
||||
settings: default
|
||||
- type: GhostRoleMobSpawner
|
||||
prototype: MobCarpQueen
|
||||
- type: Sprite
|
||||
sprite: Markers/jobs.rsi
|
||||
layers:
|
||||
- state: green
|
||||
- sprite: Mobs/Aliens/Carps/sharkminnow.rsi
|
||||
state: icon
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
- type: entity
|
||||
id: MobCarpQueen
|
||||
name: carp queen
|
||||
parent: MobShark
|
||||
description: The queen of carps, commanding a vicious shoal.
|
||||
suffix: Ghost Role
|
||||
components:
|
||||
- type: Actions
|
||||
- type: GhostRole
|
||||
makeSentient: true
|
||||
allowMovement: true
|
||||
allowSpeech: true
|
||||
name: ghost-role-information-carp-queen-name
|
||||
description: ghost-role-information-carp-queen-description
|
||||
rules: ghost-role-information-freeagent-rules
|
||||
mindRoles:
|
||||
- MindRoleGhostRoleSoloAntagonist
|
||||
raffle:
|
||||
settings: default
|
||||
- type: GhostTakeoverAvailable
|
||||
- type: Tag
|
||||
tags:
|
||||
- DoorBumpOpener
|
||||
- type: CarpQueen
|
||||
hungerPerSummon: 25
|
||||
maxArmySize: 5
|
||||
healPerHunger: 0.2
|
||||
maxHealPerTick: 5
|
||||
spawnChances:
|
||||
MobCarpServantRainbow: 80
|
||||
MobCarpServantHolo: 10
|
||||
MobCarpServantDungeon: 10
|
||||
- type: ReplacementAccent
|
||||
remove: true
|
||||
- type: Accentless
|
||||
removes:
|
||||
- type: ReplacementAccent
|
||||
accent: genericAggressive
|
||||
- type: TypingIndicator
|
||||
proto: slime
|
||||
- type: Hunger
|
||||
- type: Thirst
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
- type: entity
|
||||
id: BaseMobCarpServant
|
||||
abstract: true
|
||||
components:
|
||||
- type: CarpQueenServant
|
||||
- type: CarpServantMemory
|
||||
biteReagentAmount: 1
|
||||
- type: MeleeWeapon
|
||||
damage:
|
||||
types:
|
||||
Blunt: 2
|
||||
Slash: 3
|
||||
- type: HTN
|
||||
rootTask:
|
||||
task: RatServantCompound
|
||||
blackboard:
|
||||
IdleRange: !type:Single
|
||||
3.5
|
||||
FollowCloseRange: !type:Single
|
||||
2.0
|
||||
FollowRange: !type:Single
|
||||
3.0
|
||||
|
||||
- type: entity
|
||||
id: MobCarpServantDungeon
|
||||
parent: [ BaseMobCarpServant, MobCarpDungeon ]
|
||||
|
||||
- type: entity
|
||||
id: MobCarpServantMagic
|
||||
parent: [ BaseMobCarpServant, MobCarpMagic ]
|
||||
|
||||
- type: entity
|
||||
id: MobCarpServantHolo
|
||||
parent: [ BaseMobCarpServant, MobCarpHolo ]
|
||||
|
||||
- type: entity
|
||||
id: MobCarpServantRainbow
|
||||
parent: [ BaseMobCarpServant, MobCarpRainbow ]
|
||||
components:
|
||||
# Remove RgbLightController to allow fixed color from liquid
|
||||
- type: RgbLightController
|
||||
remove: true
|
||||
|
||||
- type: entity
|
||||
id: MobCarpServantDragon
|
||||
parent: [ BaseMobCarpServant, MobCarpDragon ]
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
- type: entity
|
||||
parent: FoodEggBase
|
||||
id: MobCarpEgg
|
||||
name: carp egg
|
||||
description: A pulsing mass, waiting to hatch.
|
||||
components:
|
||||
- type: Appearance
|
||||
- type: Sprite
|
||||
layers:
|
||||
- sprite: _Sunrise/Objects/Misc/egg_carp_queen.rsi
|
||||
state: icon
|
||||
map: [ base ]
|
||||
- sprite: _Sunrise/Objects/Misc/egg_carp_queen.rsi
|
||||
state: overlay-icon
|
||||
map: [ overlay ]
|
||||
- type: Item
|
||||
sprite: _Sunrise/Objects/Misc/egg_carp_queen.rsi
|
||||
size: Tiny
|
||||
- type: PointLight
|
||||
radius: 1.5
|
||||
energy: 3
|
||||
color: "#4faffb"
|
||||
- type: CarpEgg
|
||||
requiredVolume: 15
|
||||
checkInterval: 3
|
||||
hatchDelay: 5
|
||||
maxWaitWithoutLiquid: 30
|
||||
queenCheckRange: 3
|
||||
friendSearchRange: 3
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger: !type:DamageTrigger
|
||||
damage: 1
|
||||
behaviors:
|
||||
- !type:PlaySoundBehavior
|
||||
sound:
|
||||
collection: desecration
|
||||
- !type:SpillBehavior
|
||||
solution: food
|
||||
- !type:DoActsBehavior
|
||||
acts: [ "Destruction" ]
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Created by KAVALDi, modified from actions_rat_king.rsi and carp.rsi",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "carpQueenEgg"
|
||||
},
|
||||
{
|
||||
"name": "attack"
|
||||
},
|
||||
{
|
||||
"name": "attackOff"
|
||||
},
|
||||
{
|
||||
"name": "follow"
|
||||
},
|
||||
{
|
||||
"name": "followOff"
|
||||
},
|
||||
{
|
||||
"name": "loose"
|
||||
},
|
||||
{
|
||||
"name": "looseOff"
|
||||
},
|
||||
{
|
||||
"name": "stay"
|
||||
},
|
||||
{
|
||||
"name": "stayOff"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2 KiB |
|
After Width: | Height: | Size: 267 B |
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "by KAVALDi",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "icon"
|
||||
},
|
||||
{
|
||||
"name": "overlay-icon"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 207 B |