From 6343a5cd7ed8be89ef5317593ca97dea1cfdefd6 Mon Sep 17 00:00:00 2001 From: KAVALDi Date: Fri, 26 Dec 2025 00:17:55 +0700 Subject: [PATCH] Carp Queen (#3352) --- .../CarpQueen/CarpEggVisualizerSystem.cs | 28 + .../CarpQueen/CarpServantVisualizerSystem.cs | 85 +++ .../_Sunrise/CarpQueen/CarpEggSystem.cs | 545 ++++++++++++++++++ .../CarpQueen/CarpQueenStrengthSystem.cs | 31 + .../_Sunrise/CarpQueen/CarpQueenSystem.cs | 237 ++++++++ .../CarpQueen/CarpServantBiteSystem.cs | 57 ++ .../CarpQueen/CarpServantDisciplineSystem.cs | 103 ++++ .../CarpQueen/CarpServantRetaliationSystem.cs | 84 +++ .../CombatMode/SharedCombatModeSystem.cs | 11 + .../NPC/Systems/NpcFactionSystem.Exception.cs | 17 + .../_Sunrise/CarpQueen/CarpEggComponent.cs | 54 ++ .../_Sunrise/CarpQueen/CarpEggVisuals.cs | 11 + .../CarpQueen/CarpQueenAccessSystem.cs | 11 + .../CarpQueen/CarpQueenActionEvents.cs | 31 + .../_Sunrise/CarpQueen/CarpQueenComponent.cs | 130 +++++ .../CarpQueen/CarpQueenServantComponent.cs | 13 + .../CarpQueen/CarpServantMemoryComponent.cs | 52 ++ .../CarpQueen/SharedCarpQueenSystem.cs | 114 ++++ .../_sunrise/actions/carp_queen.ftl | 14 + .../entities/markers/spawners/ghost_roles.ftl | 3 + .../entities/mobs/npcs/carp_queen.ftl | 9 + .../_sunrise/entities/objects/misc/eggs.ftl | 3 + .../en-US/datasets/carp_queen_commands.ftl | 13 + .../_sunrise/actions/carp_queen.ftl | 12 + .../entities/markers/spawners/ghost_roles.ftl | 3 + .../entities/mobs/npcs/carp_queen.ftl | 9 + .../_sunrise/entities/objects/misc/eggs.ftl | 3 + .../ru-RU/datasets/carp_queen_commands.ftl | 13 + .../_Sunrise/Actions/carp_queen.yml | 91 +++ .../_Sunrise/Datasets/carp_queen_commands.yml | 25 + .../Entities/Markers/Spawners/ghost_roles.yml | 22 + .../Entities/Mobs/NPCs/carp_queen.yml | 44 ++ .../Entities/Mobs/NPCs/carp_servants.yml | 48 ++ .../Entities/Objects/Misc/carp_egg.yml | 44 ++ .../Actions/actions_carp_queen.rsi/attack.png | Bin 0 -> 1677 bytes .../actions_carp_queen.rsi/attackOff.png | Bin 0 -> 2223 bytes .../actions_carp_queen.rsi/carpQueenEgg.png | Bin 0 -> 489 bytes .../Actions/actions_carp_queen.rsi/follow.png | Bin 0 -> 1647 bytes .../actions_carp_queen.rsi/followOff.png | Bin 0 -> 2169 bytes .../Actions/actions_carp_queen.rsi/loose.png | Bin 0 -> 1464 bytes .../actions_carp_queen.rsi/looseOff.png | Bin 0 -> 1944 bytes .../Actions/actions_carp_queen.rsi/meta.json | 38 ++ .../Actions/actions_carp_queen.rsi/stay.png | Bin 0 -> 1929 bytes .../actions_carp_queen.rsi/stayOff.png | Bin 0 -> 2052 bytes .../Objects/Misc/egg_carp_queen.rsi/icon.png | Bin 0 -> 267 bytes .../Objects/Misc/egg_carp_queen.rsi/meta.json | 17 + .../Misc/egg_carp_queen.rsi/overlay-icon.png | Bin 0 -> 207 bytes 47 files changed, 2025 insertions(+) create mode 100644 Content.Client/_Sunrise/CarpQueen/CarpEggVisualizerSystem.cs create mode 100644 Content.Client/_Sunrise/CarpQueen/CarpServantVisualizerSystem.cs create mode 100644 Content.Server/_Sunrise/CarpQueen/CarpEggSystem.cs create mode 100644 Content.Server/_Sunrise/CarpQueen/CarpQueenStrengthSystem.cs create mode 100644 Content.Server/_Sunrise/CarpQueen/CarpQueenSystem.cs create mode 100644 Content.Server/_Sunrise/CarpQueen/CarpServantBiteSystem.cs create mode 100644 Content.Server/_Sunrise/CarpQueen/CarpServantDisciplineSystem.cs create mode 100644 Content.Server/_Sunrise/CarpQueen/CarpServantRetaliationSystem.cs create mode 100644 Content.Shared/_Sunrise/CarpQueen/CarpEggComponent.cs create mode 100644 Content.Shared/_Sunrise/CarpQueen/CarpEggVisuals.cs create mode 100644 Content.Shared/_Sunrise/CarpQueen/CarpQueenAccessSystem.cs create mode 100644 Content.Shared/_Sunrise/CarpQueen/CarpQueenActionEvents.cs create mode 100644 Content.Shared/_Sunrise/CarpQueen/CarpQueenComponent.cs create mode 100644 Content.Shared/_Sunrise/CarpQueen/CarpQueenServantComponent.cs create mode 100644 Content.Shared/_Sunrise/CarpQueen/CarpServantMemoryComponent.cs create mode 100644 Content.Shared/_Sunrise/CarpQueen/SharedCarpQueenSystem.cs create mode 100644 Resources/Locale/en-US/_prototypes/_sunrise/actions/carp_queen.ftl create mode 100644 Resources/Locale/en-US/_prototypes/_sunrise/entities/mobs/npcs/carp_queen.ftl create mode 100644 Resources/Locale/en-US/datasets/carp_queen_commands.ftl create mode 100644 Resources/Locale/ru-RU/_prototypes/_sunrise/actions/carp_queen.ftl create mode 100644 Resources/Locale/ru-RU/_prototypes/_sunrise/entities/mobs/npcs/carp_queen.ftl create mode 100644 Resources/Locale/ru-RU/datasets/carp_queen_commands.ftl create mode 100644 Resources/Prototypes/_Sunrise/Actions/carp_queen.yml create mode 100644 Resources/Prototypes/_Sunrise/Datasets/carp_queen_commands.yml create mode 100644 Resources/Prototypes/_Sunrise/Entities/Mobs/NPCs/carp_queen.yml create mode 100644 Resources/Prototypes/_Sunrise/Entities/Mobs/NPCs/carp_servants.yml create mode 100644 Resources/Prototypes/_Sunrise/Entities/Objects/Misc/carp_egg.yml create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/attack.png create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/attackOff.png create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/carpQueenEgg.png create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/follow.png create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/followOff.png create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/loose.png create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/looseOff.png create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/meta.json create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/stay.png create mode 100644 Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/stayOff.png create mode 100644 Resources/Textures/_Sunrise/Objects/Misc/egg_carp_queen.rsi/icon.png create mode 100644 Resources/Textures/_Sunrise/Objects/Misc/egg_carp_queen.rsi/meta.json create mode 100644 Resources/Textures/_Sunrise/Objects/Misc/egg_carp_queen.rsi/overlay-icon.png diff --git a/Content.Client/_Sunrise/CarpQueen/CarpEggVisualizerSystem.cs b/Content.Client/_Sunrise/CarpQueen/CarpEggVisualizerSystem.cs new file mode 100644 index 0000000000..fa1914ba24 --- /dev/null +++ b/Content.Client/_Sunrise/CarpQueen/CarpEggVisualizerSystem.cs @@ -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 +{ + [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); + } + } +} + + diff --git a/Content.Client/_Sunrise/CarpQueen/CarpServantVisualizerSystem.cs b/Content.Client/_Sunrise/CarpQueen/CarpServantVisualizerSystem.cs new file mode 100644 index 0000000000..ba54534616 --- /dev/null +++ b/Content.Client/_Sunrise/CarpQueen/CarpServantVisualizerSystem.cs @@ -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; + +/// +/// 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. +/// +public sealed class CarpServantVisualizerSystem : VisualizerSystem +{ + [Dependency] private readonly SharedPointLightSystem _lights = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnComponentAdd); + } + + private void OnComponentAdd(EntityUid uid, CarpServantMemoryComponent component, ComponentAdd args) + { + // Remove RgbLightController on client side to prevent rainbow effect + RemComp(uid); + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + // Continuously override RgbLightController color with fixed liquid color + var query = EntityQueryEnumerator(); + 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(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(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(uid, out var sprite)) + { + _sprite.LayerSetColor((uid, sprite), 0, component.LiquidColor); + } + + if (TryComp(uid, out var light)) + { + _lights.SetColor(uid, component.LiquidColor, light); + } + } +} + diff --git a/Content.Server/_Sunrise/CarpQueen/CarpEggSystem.cs b/Content.Server/_Sunrise/CarpQueen/CarpEggSystem.cs new file mode 100644 index 0000000000..7245d142a5 --- /dev/null +++ b/Content.Server/_Sunrise/CarpQueen/CarpEggSystem.cs @@ -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(OnEggDestroyed); + SubscribeLocalEvent(OnEggShutdown); + SubscribeLocalEvent(OnEggMapInit); + SubscribeLocalEvent(OnAnchorChanged); + SubscribeLocalEvent(OnRemovedFromContainer); + SubscribeLocalEvent(OnSolutionChanged); + SubscribeLocalEvent(OnServantStartup); + SubscribeLocalEvent(OnPuddleMapInit); + SubscribeLocalEvent(OnTileChanged); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + 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(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(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(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(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(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(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(ent, out var egg)) + TryHatchCheck(ent, egg); + } + } + + private void OnTileChanged(ref TileChangedEvent ev) + { + if (!TryComp(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(ent, out var egg)) + TryHatchCheck(ent, egg); + } + } + } + + + + private void TryHatchCheck(EntityUid uid, CarpEggComponent egg) + { + if (!TryComp(uid, out var xform)) + return; + + // Only hatch if not inside containers + if (xform.GridUid == null) + return; + if (_containers.IsEntityInContainer(uid)) + return; + + if (!TryComp(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(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("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 rememberedReagents = new(); + + if (TryComp(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("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(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(); + _lookup.GetEntitiesInRange(xform.Coordinates, egg.FriendSearchRange, nearbyEntities); + + var exception = EnsureComp(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(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(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(mob); + + // Use normal carp HTN compound instead of RatServantCompound + if (TryComp(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(); + 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); + } + } +} + + diff --git a/Content.Server/_Sunrise/CarpQueen/CarpQueenStrengthSystem.cs b/Content.Server/_Sunrise/CarpQueen/CarpQueenStrengthSystem.cs new file mode 100644 index 0000000000..49df5ef8bf --- /dev/null +++ b/Content.Server/_Sunrise/CarpQueen/CarpQueenStrengthSystem.cs @@ -0,0 +1,31 @@ +using Content.Shared._Sunrise.CarpQueen; +using Content.Shared.CombatMode; +using Content.Shared.Movement.Components; + +namespace Content.Server._Sunrise.CarpQueen; + +/// +/// System that increases Carp Queen's pushing strength when in combat mode. +/// +public sealed class CarpQueenStrengthSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnCombatModeChanged); + } + + private void OnCombatModeChanged(EntityUid uid, CarpQueenComponent component, ref CombatModeChangedEvent args) + { + if (!TryComp(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); + } +} + diff --git a/Content.Server/_Sunrise/CarpQueen/CarpQueenSystem.cs b/Content.Server/_Sunrise/CarpQueen/CarpQueenSystem.cs new file mode 100644 index 0000000000..f5a9a129fb --- /dev/null +++ b/Content.Server/_Sunrise/CarpQueen/CarpQueenSystem.cs @@ -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(OnSummon); + SubscribeLocalEvent(OnPointedAt); + SubscribeLocalEvent(OnServantShutdown); + } + + protected override void OnStartup(EntityUid uid, CarpQueenComponent component, ComponentStartup args) + { + base.OnStartup(uid, component, args); + + if (TryComp(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(); + var aliveServants = 0; + foreach (var s in component.Servants) + { + if (!Exists(s)) + { + toRemoveServants.Add(s); + continue; + } + if (TryComp(s, out var mobState) && mobState.CurrentState == MobState.Dead) + continue; + aliveServants++; + } + foreach (var rem in toRemoveServants) + component.Servants.Remove(rem); + + var toRemoveEggs = new List(); + 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(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(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(target, out var mobState)) + valid = mobState.CurrentState != MobState.Dead; + else if (HasComp(target)) + valid = true; + else if (HasComp(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(servant, out var memory)) + { + var exception = EnsureComp(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(); + 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(uid, out var servant) || servant.Queen == null || !Exists(servant.Queen.Value)) + return; + + if (!TryComp(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(datasetId, out var datasetPrototype)) + return; + + var msg = Random.Pick(datasetPrototype); + _chat.TrySendInGameICMessage(uid, msg, InGameICChatType.Speak, true); + } +} + + diff --git a/Content.Server/_Sunrise/CarpQueen/CarpServantBiteSystem.cs b/Content.Server/_Sunrise/CarpQueen/CarpServantBiteSystem.cs new file mode 100644 index 0000000000..40a27d8b2d --- /dev/null +++ b/Content.Server/_Sunrise/CarpQueen/CarpServantBiteSystem.cs @@ -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; + +/// +/// System that handles carp servant bite mechanics: +/// Injects 1u of each remembered reagent from the liquid the carp hatched from. +/// +public sealed class CarpServantBiteSystem : EntitySystem +{ + [Dependency] private readonly BloodstreamSystem _bloodstream = default!; + [Dependency] private readonly IPrototypeManager _protos = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(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(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(reagentId)) + { + solution.AddReagent(reagentId, memory.BiteReagentAmount); + } + } + + if (solution.Volume > FixedPoint2.Zero) + { + _bloodstream.TryAddToChemicals((target, bloodstream), solution); + } + } + } +} + diff --git a/Content.Server/_Sunrise/CarpQueen/CarpServantDisciplineSystem.cs b/Content.Server/_Sunrise/CarpQueen/CarpServantDisciplineSystem.cs new file mode 100644 index 0000000000..fa4a6e6869 --- /dev/null +++ b/Content.Server/_Sunrise/CarpQueen/CarpServantDisciplineSystem.cs @@ -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; + +/// +/// 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. +/// +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(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(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(uid, out meleeCombat) && meleeCombat.Target != EntityUid.Invalid) + { + currentTarget = meleeCombat.Target; + } + // Fallback to blackboard - check common target keys + else if (TryComp(uid, out htn)) + { + // Try CurrentOrderedTarget first (set by queen/orders) + if (htn.Blackboard.TryGetValue(NPCBlackboard.CurrentOrderedTarget, out var orderedTarget, EntityManager) && orderedTarget != EntityUid.Invalid) + currentTarget = orderedTarget; + // Then try generic "Target" key + else if (htn.Blackboard.TryGetValue("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(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); + } + } + } +} + diff --git a/Content.Server/_Sunrise/CarpQueen/CarpServantRetaliationSystem.cs b/Content.Server/_Sunrise/CarpQueen/CarpServantRetaliationSystem.cs new file mode 100644 index 0000000000..a56925e1b5 --- /dev/null +++ b/Content.Server/_Sunrise/CarpQueen/CarpServantRetaliationSystem.cs @@ -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; + +/// +/// System that makes tamed carps retaliate against entities that damage their remembered friends. +/// +public sealed class CarpServantRetaliationSystem : EntitySystem +{ + [Dependency] private readonly NpcFactionSystem _npcFaction = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(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(attacker)) + return; + + // Find all carps that remember the damaged entity as a friend + var query = EntityQueryEnumerator(); + 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(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(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)); + } + } +} + diff --git a/Content.Shared/CombatMode/SharedCombatModeSystem.cs b/Content.Shared/CombatMode/SharedCombatModeSystem.cs index d5452524ce..709c420038 100644 --- a/Content.Shared/CombatMode/SharedCombatModeSystem.cs +++ b/Content.Shared/CombatMode/SharedCombatModeSystem.cs @@ -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 { } + +/// +/// Raised when combat mode changes for an entity. +/// +[ByRefEvent] +public readonly record struct CombatModeChangedEvent(bool IsInCombatMode); diff --git a/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs b/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs index e69f0c2f7a..23c970ec5e 100644 --- a/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs +++ b/Content.Shared/NPC/Systems/NpcFactionSystem.Exception.cs @@ -120,6 +120,23 @@ public sealed partial class NpcFactionSystem tracker.Entities.Remove(ent); } + // Sunrise-Start + /// + /// Makes an entity no longer be ignored, if it was. + /// Allows the NPC to attack the entity again. + /// + public void UnignoreEntity(Entity 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 + /// /// Makes a list of entities no longer be considered hostile, if it was. /// Doesn't apply to regular faction hostilities. diff --git a/Content.Shared/_Sunrise/CarpQueen/CarpEggComponent.cs b/Content.Shared/_Sunrise/CarpQueen/CarpEggComponent.cs new file mode 100644 index 0000000000..a8203b3d59 --- /dev/null +++ b/Content.Shared/_Sunrise/CarpQueen/CarpEggComponent.cs @@ -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; + + /// + /// Required puddle volume (u) to hatch. + /// + [DataField("requiredVolume")] public float RequiredVolume = 15f; + + /// + /// Seconds between hatch checks. + /// + [DataField("checkInterval")] public float CheckInterval = 3f; + + [DataField("accum")] public float Accum; + + /// + /// Seconds the egg must remain on valid liquid before hatching. + /// + [DataField("hatchDelay")] public float HatchDelay = 5f; + + /// + /// Whether current tile conditions are sufficient for hatching. + /// + [DataField("eligible")] public bool Eligible; + + /// + /// Accumulated time spent waiting without valid liquid. If exceeds MaxWaitWithoutLiquid, egg breaks. + /// + [DataField("waitElapsed")] public float WaitElapsed; + + /// + /// Max seconds to wait for liquid to appear before breaking the egg. + /// + [DataField("maxWaitWithoutLiquid")] public float MaxWaitWithoutLiquid = 30f; + + /// + /// 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. + /// + [DataField("queenCheckRange")] public float QueenCheckRange = 3f; + + /// + /// Range (in tiles) to search for nearby players to imprint on when queen is not nearby. + /// + [DataField("friendSearchRange")] public float FriendSearchRange = 3f; +} + + diff --git a/Content.Shared/_Sunrise/CarpQueen/CarpEggVisuals.cs b/Content.Shared/_Sunrise/CarpQueen/CarpEggVisuals.cs new file mode 100644 index 0000000000..f7142999ba --- /dev/null +++ b/Content.Shared/_Sunrise/CarpQueen/CarpEggVisuals.cs @@ -0,0 +1,11 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared._Sunrise.CarpQueen; + +[Serializable, NetSerializable] +public enum CarpEggVisuals +{ + OverlayColor +} + + diff --git a/Content.Shared/_Sunrise/CarpQueen/CarpQueenAccessSystem.cs b/Content.Shared/_Sunrise/CarpQueen/CarpQueenAccessSystem.cs new file mode 100644 index 0000000000..73fc95be25 --- /dev/null +++ b/Content.Shared/_Sunrise/CarpQueen/CarpQueenAccessSystem.cs @@ -0,0 +1,11 @@ +namespace Content.Shared._Sunrise.CarpQueen; + +/// +/// Marker system that grants access permissions to mutate Carp Queen components from server systems. +/// Server systems that need write access should inherit from this. +/// +public abstract class CarpQueenAccessSystem : EntitySystem +{ +} + + diff --git a/Content.Shared/_Sunrise/CarpQueen/CarpQueenActionEvents.cs b/Content.Shared/_Sunrise/CarpQueen/CarpQueenActionEvents.cs new file mode 100644 index 0000000000..86acc434c9 --- /dev/null +++ b/Content.Shared/_Sunrise/CarpQueen/CarpQueenActionEvents.cs @@ -0,0 +1,31 @@ +using Content.Shared.Actions; +using Robust.Shared.Serialization; + +namespace Content.Shared._Sunrise.CarpQueen; + +public sealed partial class CarpQueenSummonActionEvent : InstantActionEvent +{ +} + +/// +/// Event for carp queen order actions (Stay, Follow, Kill, Loose). +/// +public sealed partial class CarpQueenOrderActionEvent : InstantActionEvent +{ + /// + /// The type of order being given + /// + [DataField("type")] + public CarpQueenOrderType Type; +} + +[Serializable, NetSerializable] +public enum CarpQueenOrderType : byte +{ + Stay, + Follow, + Kill, + Loose +} + + diff --git a/Content.Shared/_Sunrise/CarpQueen/CarpQueenComponent.cs b/Content.Shared/_Sunrise/CarpQueen/CarpQueenComponent.cs new file mode 100644 index 0000000000..f25f690e97 --- /dev/null +++ b/Content.Shared/_Sunrise/CarpQueen/CarpQueenComponent.cs @@ -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))] + public string ActionSummon = "ActionCarpQueenSummon"; + + [DataField("actionSummonEntity")] + public EntityUid? ActionSummonEntity; + + [DataField("actionOrderStay", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ActionOrderStay = "ActionCarpQueenOrderStay"; + + [DataField("actionOrderStayEntity")] + public EntityUid? ActionOrderStayEntity; + + [DataField("actionOrderFollow", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ActionOrderFollow = "ActionCarpQueenOrderFollow"; + + [DataField("actionOrderFollowEntity")] + public EntityUid? ActionOrderFollowEntity; + + [DataField("actionOrderKill", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ActionOrderKill = "ActionCarpQueenOrderKill"; + + [DataField("actionOrderKillEntity")] + public EntityUid? ActionOrderKillEntity; + + [DataField("actionOrderLoose", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ActionOrderLoose = "ActionCarpQueenOrderLoose"; + + [DataField("actionOrderLooseEntity")] + public EntityUid? ActionOrderLooseEntity; + + /// + /// Current order applied to servants. + /// + [DataField("currentOrders"), AutoNetworkedField] + public CarpQueenOrderType CurrentOrder = CarpQueenOrderType.Loose; + + /// + /// List of spawned servants controlled by the queen. + /// + [DataField("servants")] + public HashSet Servants = new(); + + /// + /// Active eggs spawned by the queen (not yet hatched). + /// + [DataField("eggs")] + public HashSet Eggs = new(); + + /// + /// Pool of carp servant prototype IDs to randomly pick from when summoning. + /// + [DataField("armyMobSpawnOptions")] + public List ArmyMobSpawnOptions = new() + { + "MobCarpServantDungeon", + "MobCarpServantMagic", + "MobCarpServantHolo", + "MobCarpServantRainbow", + "MobCarpServantDragon" + }; + + /// + /// Dataset mapping for order callouts (spoken lines on order change). + /// + [DataField("orderCallouts")] + public Dictionary OrderCallouts = new() + { + { CarpQueenOrderType.Stay, "CarpQueenCommandStay" }, + { CarpQueenOrderType.Follow, "CarpQueenCommandFollow" }, + { CarpQueenOrderType.Kill, "CarpQueenCommandKill" }, + { CarpQueenOrderType.Loose, "CarpQueenCommandLoose" } + }; + + /// + /// Hunger consumed per summon use. + /// + [DataField("hungerPerSummon")] + public float HungerPerSummon = 25f; + + /// + /// Tracks last observed hunger to grant small healing when eating. + /// Server-side only; not networked. + /// + public float LastObservedHunger; + + /// + /// Maximum total servants + eggs the queen can have at once. + /// + [DataField("maxArmySize")] + public int MaxArmySize = 5; + + /// + /// HP healed per 1 unit of hunger gained (when eating). + /// + [DataField("healPerHunger")] + public float HealPerHunger = 0.2f; + + /// + /// Maximum HP healed per tick from eating. + /// + [DataField("maxHealPerTick")] + public float MaxHealPerTick = 5f; + + /// + /// 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). + /// + [DataField("spawnChances")] + public Dictionary SpawnChances = new() + { + { "MobCarpServantRainbow", 80 }, + { "MobCarpServantHolo", 10 }, + { "MobCarpServantDungeon", 10 } + }; +} + + diff --git a/Content.Shared/_Sunrise/CarpQueen/CarpQueenServantComponent.cs b/Content.Shared/_Sunrise/CarpQueen/CarpQueenServantComponent.cs new file mode 100644 index 0000000000..15942f1c3b --- /dev/null +++ b/Content.Shared/_Sunrise/CarpQueen/CarpQueenServantComponent.cs @@ -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; +} + + diff --git a/Content.Shared/_Sunrise/CarpQueen/CarpServantMemoryComponent.cs b/Content.Shared/_Sunrise/CarpQueen/CarpServantMemoryComponent.cs new file mode 100644 index 0000000000..e3ff74008c --- /dev/null +++ b/Content.Shared/_Sunrise/CarpQueen/CarpServantMemoryComponent.cs @@ -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; + +/// +/// Component that stores memory of the liquid the carp hatched from, +/// including its color and reagents for injection on bite. +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class CarpServantMemoryComponent : Component +{ + /// + /// Color of the liquid the carp hatched from. + /// Used for visual appearance. + /// + [DataField("liquidColor"), AutoNetworkedField] + public Color LiquidColor = Color.White; + + /// + /// Dictionary of reagent IDs and their amounts that were in the liquid. + /// Used for injection on bite. + /// + [DataField("rememberedReagents"), AutoNetworkedField] + public Dictionary RememberedReagents = new(); + + /// + /// Amount of each remembered reagent to inject per bite (in units). + /// + [DataField("biteReagentAmount")] + public FixedPoint2 BiteReagentAmount = FixedPoint2.New(1); + + /// + /// 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. + /// + [DataField("rememberedFriends"), AutoNetworkedField] + public HashSet RememberedFriends = new(); + + /// + /// List of entities that the carp is temporarily forbidden to attack. + /// These are cleared when the attacker damages the carp's owner. + /// + [DataField("forbiddenTargets")] + public HashSet ForbiddenTargets = new(); +} + diff --git a/Content.Shared/_Sunrise/CarpQueen/SharedCarpQueenSystem.cs b/Content.Shared/_Sunrise/CarpQueen/SharedCarpQueenSystem.cs new file mode 100644 index 0000000000..b863b0dc8a --- /dev/null +++ b/Content.Shared/_Sunrise/CarpQueen/SharedCarpQueenSystem.cs @@ -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(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(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(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) + { + } + + /// + /// Converts CarpQueenOrderType to RatKingOrderType for HTN compatibility. + /// HTN compounds use RatKingOrderType, so we need to map our order types to them. + /// + 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 + }; + } +} + + diff --git a/Resources/Locale/en-US/_prototypes/_sunrise/actions/carp_queen.ftl b/Resources/Locale/en-US/_prototypes/_sunrise/actions/carp_queen.ftl new file mode 100644 index 0000000000..8a3ffbdf65 --- /dev/null +++ b/Resources/Locale/en-US/_prototypes/_sunrise/actions/carp_queen.ftl @@ -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! + + diff --git a/Resources/Locale/en-US/_prototypes/_sunrise/entities/markers/spawners/ghost_roles.ftl b/Resources/Locale/en-US/_prototypes/_sunrise/entities/markers/spawners/ghost_roles.ftl index a460902719..9f33875000 100644 --- a/Resources/Locale/en-US/_prototypes/_sunrise/entities/markers/spawners/ghost_roles.ftl +++ b/Resources/Locale/en-US/_prototypes/_sunrise/entities/markers/spawners/ghost_roles.ftl @@ -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 } diff --git a/Resources/Locale/en-US/_prototypes/_sunrise/entities/mobs/npcs/carp_queen.ftl b/Resources/Locale/en-US/_prototypes/_sunrise/entities/mobs/npcs/carp_queen.ftl new file mode 100644 index 0000000000..6309e63ac2 --- /dev/null +++ b/Resources/Locale/en-US/_prototypes/_sunrise/entities/mobs/npcs/carp_queen.ftl @@ -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. diff --git a/Resources/Locale/en-US/_prototypes/_sunrise/entities/objects/misc/eggs.ftl b/Resources/Locale/en-US/_prototypes/_sunrise/entities/objects/misc/eggs.ftl index 5ca0849d82..11d376f2a3 100644 --- a/Resources/Locale/en-US/_prototypes/_sunrise/entities/objects/misc/eggs.ftl +++ b/Resources/Locale/en-US/_prototypes/_sunrise/entities/objects/misc/eggs.ftl @@ -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. diff --git a/Resources/Locale/en-US/datasets/carp_queen_commands.ftl b/Resources/Locale/en-US/datasets/carp_queen_commands.ftl new file mode 100644 index 0000000000..85d0721613 --- /dev/null +++ b/Resources/Locale/en-US/datasets/carp_queen_commands.ftl @@ -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... + + diff --git a/Resources/Locale/ru-RU/_prototypes/_sunrise/actions/carp_queen.ftl b/Resources/Locale/ru-RU/_prototypes/_sunrise/actions/carp_queen.ftl new file mode 100644 index 0000000000..eead078328 --- /dev/null +++ b/Resources/Locale/ru-RU/_prototypes/_sunrise/actions/carp_queen.ftl @@ -0,0 +1,12 @@ +ent-ActionCarpQueenSummon = Отложить икру + .desc = Откладывает икру, которая при попадании в жидкость вылупляется в случайного карпа. +ent-ActionCarpQueenOrderStay = Отдых + .desc = Приказывает карпам-слугам игнорировать угрозы. +ent-ActionCarpQueenOrderFollow = Следуй + .desc = Приказывает карпам-слугам следовать вплотную за вами. +ent-ActionCarpQueenOrderKill = Убейте его + .desc = Приказывает карпам-слугам атаковать того, на кого вы укажете. +ent-ActionCarpQueenOrderLoose = Вольно + .desc = Приказывает карпам-слугам следовать по велению матушки-природы. + +carp-queen-summon-popup = Вы отложили икру! diff --git a/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/markers/spawners/ghost_roles.ftl b/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/markers/spawners/ghost_roles.ftl index dda825dedc..eb0f572566 100644 --- a/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/markers/spawners/ghost_roles.ftl +++ b/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/markers/spawners/ghost_roles.ftl @@ -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 } diff --git a/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/mobs/npcs/carp_queen.ftl b/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/mobs/npcs/carp_queen.ftl new file mode 100644 index 0000000000..3bdd4784b5 --- /dev/null +++ b/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/mobs/npcs/carp_queen.ftl @@ -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 } слуг и икринок вместе. diff --git a/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/objects/misc/eggs.ftl b/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/objects/misc/eggs.ftl index d6613cc1de..12131534a7 100644 --- a/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/objects/misc/eggs.ftl +++ b/Resources/Locale/ru-RU/_prototypes/_sunrise/entities/objects/misc/eggs.ftl @@ -1,2 +1,5 @@ ent-EggSpiderFertilized = яйцо паука .desc = Это драгоценный камень? Или яйцо? Выглядит дорого. + +ent-MobCarpEgg = икра карпа + .desc = Пульсирующая масса, ожидающая вылупления. diff --git a/Resources/Locale/ru-RU/datasets/carp_queen_commands.ftl b/Resources/Locale/ru-RU/datasets/carp_queen_commands.ftl new file mode 100644 index 0000000000..cd02f7eb08 --- /dev/null +++ b/Resources/Locale/ru-RU/datasets/carp_queen_commands.ftl @@ -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 = Гр-рр... + + diff --git a/Resources/Prototypes/_Sunrise/Actions/carp_queen.yml b/Resources/Prototypes/_Sunrise/Actions/carp_queen.yml new file mode 100644 index 0000000000..da4042b359 --- /dev/null +++ b/Resources/Prototypes/_Sunrise/Actions/carp_queen.yml @@ -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 + + diff --git a/Resources/Prototypes/_Sunrise/Datasets/carp_queen_commands.yml b/Resources/Prototypes/_Sunrise/Datasets/carp_queen_commands.yml new file mode 100644 index 0000000000..9abd363973 --- /dev/null +++ b/Resources/Prototypes/_Sunrise/Datasets/carp_queen_commands.yml @@ -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 + + diff --git a/Resources/Prototypes/_Sunrise/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/_Sunrise/Entities/Markers/Spawners/ghost_roles.yml index 270c1f0acf..7d2b9cf0ce 100644 --- a/Resources/Prototypes/_Sunrise/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/_Sunrise/Entities/Markers/Spawners/ghost_roles.yml @@ -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 \ No newline at end of file diff --git a/Resources/Prototypes/_Sunrise/Entities/Mobs/NPCs/carp_queen.yml b/Resources/Prototypes/_Sunrise/Entities/Mobs/NPCs/carp_queen.yml new file mode 100644 index 0000000000..d4bf440233 --- /dev/null +++ b/Resources/Prototypes/_Sunrise/Entities/Mobs/NPCs/carp_queen.yml @@ -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 + + diff --git a/Resources/Prototypes/_Sunrise/Entities/Mobs/NPCs/carp_servants.yml b/Resources/Prototypes/_Sunrise/Entities/Mobs/NPCs/carp_servants.yml new file mode 100644 index 0000000000..faef41c35a --- /dev/null +++ b/Resources/Prototypes/_Sunrise/Entities/Mobs/NPCs/carp_servants.yml @@ -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 ] + + diff --git a/Resources/Prototypes/_Sunrise/Entities/Objects/Misc/carp_egg.yml b/Resources/Prototypes/_Sunrise/Entities/Objects/Misc/carp_egg.yml new file mode 100644 index 0000000000..f1ef8ccf24 --- /dev/null +++ b/Resources/Prototypes/_Sunrise/Entities/Objects/Misc/carp_egg.yml @@ -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" ] + + + diff --git a/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/attack.png b/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/attack.png new file mode 100644 index 0000000000000000000000000000000000000000..0356b6995a3ce852743308fc26b9bf756496df6e GIT binary patch literal 1677 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/carpQueenEgg.png b/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/carpQueenEgg.png new file mode 100644 index 0000000000000000000000000000000000000000..61ea8907a546103e62cd5962572223b29d2cbb26 GIT binary patch literal 489 zcmV3{3z{o(oWKf$>A#@3&h#-GOAw-v`hz>#Fu|u7^MObt&|3HM@q94$qlU*W; z2&^tqbWzHZV5e`UCNwl>jF4c3#o2ei`QCS*PuXQ?@P{?nA8r7bfJ?w7;1c-11nyHa zJm@LFp@+$h(}|wx^}24Ru1&Y8S#C$i!2&k0w=JJ%S8ZJgPp%sGGu|M~oP!n4A~9f} z%mo6AjzA7nEf*!H9q;4_}?En({C}H=9fbU}^JlXa{f(344DG4q+F>C3#;Cygdw%&crz6>jgI&hjswIGr{rVSJo1J z!8wJum54}P_){$Me|65#5A%*Kw^%JtuNzh1ca_ad?)lP0;a!e)B2ht fFl}gB?*@1W_2#u9b`!7*00000NkvXXu0mjf^)BC{ literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/follow.png b/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/follow.png new file mode 100644 index 0000000000000000000000000000000000000000..b3a3b0194a88465fbbdec082ea91738fbbcbc190 GIT binary patch literal 1647 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/loose.png b/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/loose.png new file mode 100644 index 0000000000000000000000000000000000000000..3b39253539f4620d77adf5e25d0018c79d491c25 GIT binary patch literal 1464 zcmV;p1xNacP)(u92@>L9Cb<_)iKx8KVdrsn^5AhbGe7LBsT+l11jg0OI03PXNOJjRF`4u|Cy0OJs$(WG%q!G(`Ww2u!`nwVG7xEZ@H zj6Jpz6mTmz(BI$V4HU{FSZWK0i;(*?){PY7@4_;$aVuhZZ11q{-+Jd( zIdytcS>VXZ;!!QI7laLwdSbELhagO~w5H?0v!hb2Ru#X$tLd27wW!s>k(c+& zXA=|38spxysaNXPu7?aV4Sc%0yOEN*T&OK7cdb>xvw$va&oOcoxo4mQo4ZsQ+w*yO z_k+pAVQGj8WOyK_PH(*>EB*ZgQmfUZy`x_ictC(HAu3f#f=Ja{2gS8F~GTy!YW`)9D?^z4>10>gtwy-9+AK ztl-a=3$sfGxNR>jji@j_VtPHo-g*BPY+9DKw%cqT%|{>dJ2y9{-yhxku)KQobYwBM z@;Qi-6+*UL-q<5d3o@Lw0zetE`wb6tD&_Iq(Kee;Pj3&Dd`%6oG9OnE??)`qw$mQN zjn^rUW1eN&h;SS?x8fqIz=zrGLph~>dbX~4?d;6S;^LAF4Gzhr%d_(L3S0wBRNvN- zJD)=)T(oAwYoK$&x!xU8`+zng)8}X_zpY)eSu|4o@o{j)M)@hJ2lvO(*SI_6{j(`n_6TZwt{Up-L$c`+ip#o^2h*56SrNqHLczB z4-n>oHr*5$ijNIiLTuWFqLNrd0R`vY{(k5C?#zI=_8Po%zx#dX{Lb$@uOqMda;X1< zXvBXz+$JGU#KTI+@x+&q_`Zna_~PK7?|I^cLV37 n9UGwl8cRk>X=i^BwV-wfy z<>561>9(8WV{-Y*6-T%A_4WDJM#p7kQ#Axan5UwRP(a}(0usroK$WBKp^tir+5(_T zJ~=?(Q4`;&W!Okm_*uI8k2JT1B{7h&0D5};(aC#2W>nJRoRbqb?3Kpo6;bQajao*b3a7IhPebPO067jCHn1AzUT1{j1v z4D1_+#R}&2R|9Cf|nh%dDHiv!0zb&&Z;9N24xP?X8lFm#*3qU{>AW%kd+-Q3oso zJxIeW(vV`2$tLE(^~-KJ6{0gi%;9-_w%1f%~uy>=_wVOI?4>mUPpbw6@9Nk z=I7@nJBe(=t8g>Yu-|4uFI+GR7f`QJZmVCR2B}4Bskb-)I`Pd}NsVN5VfW6R@?h?v z@?=ey)zwvT-4e!_gS9d}JuPKrTja@;r-*}PX@neB$f@Zi$pOLf zqs4Wgr`sQ!e5h?Q3}ls++a>+mU+TxUcXqLz<*hF-FO$E&J1_5E_*H)VWjz>Qnhx)f zZQCkjVZm6Q%e}y#C%0#o47WCc!l;Brr5I_LMH65o?4gK_;==j-V6=?brn2tX99q+O42`o2F3UJN*Fx zl{>a0k{_u6LX9hkpYk0b-?r(y^#z1&)+P>Lvl%7wSg(&}&Yc-qdBGvTFXZF?x{ zKIL_hL1(EoO{G9<0mKINLb0NmdgAt!u|w(y^sa2_TN|ra(=$X?e>omxc>5QzD?w8IJ zY%*pbG$MFN(E9tGBqO4Fs^?H zcG9#s3SoF3>xiTNCy|P$lo7HtjP-aV#$K*qLWrXz*oj5~GLweokO?`%V4?XK0jwKJ zyV?%{T~cDOJO#xuSet^iE142v12L!j^(l)CWQnUiG2Q%ho}Vv1hb0hM$? zEE?C~WK0Y}aV!!sE+{e9rohT`=K$D*91xRC4TI5xXhwTvASgtA05Fbrtc6(F!!O&B zSp__%khbVx6v17i+0+b9T{?numf&gEo@e1PL$juyhl#|_+ekxBL`6gCjnOgChCt%* z$}&JK{6<70(gKAR=DE1*+(#`KQc+nrKbrZWD0#K76QCKH19?2g&$Y7LztVKVnTN!ZP2Bg()T&WX(rzadq+J+GtiTk7Vo| eZftrj*Z=zAluq0!Yz+Va002ovPDHLkV1fXnOs^0C literal 0 HcmV?d00001 diff --git a/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/meta.json b/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/meta.json new file mode 100644 index 0000000000..499e187add --- /dev/null +++ b/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/meta.json @@ -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" + } + ] +} diff --git a/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/stay.png b/Resources/Textures/_Sunrise/Interface/Actions/actions_carp_queen.rsi/stay.png new file mode 100644 index 0000000000000000000000000000000000000000..19e70781a264d2f5c1b4539943483b1ad0368c06 GIT binary patch literal 1929 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 literal 0 HcmV?d00001