public static void DestroyAllDecals( Vector2Ushort tilePosition, StaticObjectLayoutReadOnly layout) { Api.ValidateIsServer(); foreach (var tileOffset in layout.TileOffsets) { var tile = World.GetTile(tilePosition.X + tileOffset.X, tilePosition.Y + tileOffset.Y); if (!tile.IsValidTile) { continue; } var staticObjects = tile.StaticObjects; if (!HasFloorDecals(staticObjects)) { continue; } // remove floor decals using var tempList = Api.Shared.WrapInTempList(staticObjects); foreach (var staticWorldObject in tempList.AsList()) { if (staticWorldObject.ProtoStaticWorldObject.Kind == StaticObjectKind.FloorDecal) { World.DestroyObject(staticWorldObject); } } } bool HasFloorDecals(ReadOnlyListWrapper <IStaticWorldObject> staticObjects) { foreach (var staticWorldObject in staticObjects) { if (staticWorldObject.ProtoStaticWorldObject.Kind == StaticObjectKind.FloorDecal) { return(true); } } return(false); } }
private static bool SharedIsWithinInteractionDistance( ICharacter character, Vector2Ushort tilePosition, out bool obstaclesOnTheWay) { var interactionAreaShape = character.PhysicsBody.Shapes.FirstOrDefault( s => s.CollisionGroup == CollisionGroups.CharacterInteractionArea); if (interactionAreaShape is null) { // no interaction area shape (probably a spectator character) obstaclesOnTheWay = false; return(false); } var penetration = character.PhysicsBody.PhysicsSpace.TestShapeCollidesWithShape( sourceShape: interactionAreaShape, targetShape: new CircleShape( center: tilePosition.ToVector2D() + (0.5, 0.5), radius: ClickAreaRadius, collisionGroup: CollisionGroups.ClickArea), sourceShapeOffset: character.PhysicsBody.Position); if (!penetration.HasValue) { // outside of interaction area obstaclesOnTheWay = false; return(false); } // check that there are no other objects on the way between them (defined by default layer) var physicsSpace = character.PhysicsBody.PhysicsSpace; var characterCenter = character.Position + character.PhysicsBody.CenterOffset; var worldObjectCenter = (Vector2D)tilePosition + new Vector2D(0.5, 0.5); var worldObjectPointClosestToCharacter = new BoundsInt(tilePosition, Vector2Int.One) .ClampInside(characterCenter); obstaclesOnTheWay = ObstacleTestHelper.SharedHasObstaclesOnTheWay(characterCenter, physicsSpace, worldObjectCenter, worldObjectPointClosestToCharacter, sendDebugEvents: false); return(!obstaclesOnTheWay); }
public static CreateItemResult TryDropToCharacterOrGround( IReadOnlyDropItemsList dropItemsList, ICharacter toCharacter, Vector2Ushort tilePosition, bool sendNotificationWhenDropToGround, double probabilityMultiplier, DropItemContext context, out IItemsContainer groundContainer) { CreateItemResult result; if (toCharacter != null) { result = TryDropToCharacter( dropItemsList, toCharacter, sendNoFreeSpaceNotification: false, probabilityMultiplier, context); if (result.IsEverythingCreated) { groundContainer = null; return(result); } // cannot add to character - rollback and drop on ground instead result.Rollback(); } result = TryDropToGround(dropItemsList, tilePosition, probabilityMultiplier, context, out groundContainer); if (sendNotificationWhenDropToGround && result.TotalCreatedCount > 0) { // notify player that there were not enough space in inventory so the items were dropped to the ground NotificationSystem.ServerSendNotificationNoSpaceInInventoryItemsDroppedToGround( toCharacter, result.ItemAmounts.FirstOrDefault().Key?.ProtoItem); } return(result); }
protected override void PrepareProtoTile(Settings settings) { settings.AmbientSoundProvider = new TileAmbientSoundProvider( new AmbientSoundPreset(new SoundResource("Ambient/Pit"))); settings.AddGroundTexture( new ProtoTileGroundTexture( texture: GroundTexture1, blendMaskTexture: BlendMaskTextureGeneric2Smooth, noiseSelector: null)); // add clay stones decals var clayStonesTextures = ProtoTileDecal.CollectTextures("Terrain/Clay/ClayStones*"); var clayStonesSize = new Vector2Ushort(2, 2); var clayStonesNoiseSelector = new NoiseSelector( from: 0.5, to: 0.55, noise: new PerlinNoise(seed: 642571234, scale: 5, octaves: 4, persistance: 0.9, lacunarity: 3.9)); for (ushort x = 0; x <= 1; x++) { for (ushort y = 0; y <= 1; y++) { settings.AddDecal( new ProtoTileDecal(clayStonesTextures, size: clayStonesSize, offset: (x, y), noiseSelector: clayStonesNoiseSelector)); } } // add clay pit decals settings.AddDecal( new ProtoTileDecal("Terrain/Clay/ClayPit*", size: (2, 2), drawOrder: DrawOrder.GroundDecalsUnder, noiseSelector: new NoiseSelector( from: 0.95, to: 1, noise: new WhiteNoise(seed: 306721460)))); }
private bool ServerRemote_ApplyWatering(Vector2Ushort tilePosition, IItem item) { var character = ServerRemoteContext.Character; this.ServerValidateItemForRemoteCall(item, character); var result = SharedGetAvailablePlantAt(tilePosition, character); if (!result.IsSuccess) { Logger.Warning(result.ErrorTitle + ": " + result.ErrorMessage, character); return(false); } var objectPlant = result.ObjectPlant; var protoPlant = (IProtoObjectPlant)objectPlant.ProtoStaticWorldObject; var plantPrivateState = objectPlant.GetPrivateState <PlantPrivateState>(); // validate if can apply this var wateringDuration = this.WateringDuration; if (plantPrivateState.ServerTimeWateringEnds >= double.MaxValue) { // already applied this.CallClient(character, _ => _.ClientRemote_CannotApplyAlreadyApplied()); return(false); } if (plantPrivateState.ProducedHarvestsCount == protoPlant.NumberOfHarvests && protoPlant.NumberOfHarvests > 0) { // no need to apply - last harvest this.CallClient(character, _ => _.ClientRemote_CannotApplyLastHarvest()); return(false); } // apply watering ServerItemUseObserver.NotifyItemUsed(character, item); Server.Items.SetCount(item, item.Count - 1); protoPlant.ServerOnWatered(character, objectPlant, wateringDuration: wateringDuration); // notify client Logger.Important($"Watering applied: {this} to {objectPlant}"); return(true); }
private void ServerRemote_PlaceAt(IItem item, Vector2Ushort tilePosition) { var character = ServerRemoteContext.Character; this.ServerValidateItemForRemoteCall(item, character); this.SharedIsValidPlacementPosition( tilePosition, character, logErrors: true, canPlace: out var canPlace, isTooFar: out var isTooFar, errorCodeOrMessage: out _); if (!canPlace || isTooFar) { return; } var plantObject = Server.World.CreateStaticWorldObject(this.ObjectPlantProto, tilePosition); if (this.ObjectPlantProto is IProtoObjectPlant protoFarmPlant) { protoFarmPlant.ServerSetBonusForCharacter(plantObject, character); } Logger.Important($"{character} has placed plant {plantObject} from seed {item}"); this.ServerNotifyItemUsed(character, item); // decrease item count Server.Items.SetCount(item, (ushort)(item.Count - 1)); character.ServerAddSkillExperience <SkillFarming>(SkillFarming.ExperienceForSeedPlanting); // restore structure points and reset decay for the farm(s) in the tile where the seed was planted foreach (var tileObject in plantObject.OccupiedTile.StaticObjects) { if (tileObject.ProtoStaticWorldObject is not IProtoObjectFarm protoFarm) { continue; } tileObject.GetPublicState <StaticObjectPublicState>() .StructurePointsCurrent = protoFarm.SharedGetStructurePointsMax(tileObject); } }
private static bool ClampTextureSize(ref Vector2Ushort textureSize, out double scale) { var size = Math.Max(textureSize.X, textureSize.Y); var maxSize = MaxIconTextureSIze; if (size < maxSize) { scale = 1; return(false); } scale = Math.Min(maxSize / (double)textureSize.X, maxSize / (double)textureSize.Y); textureSize = new Vector2Ushort((ushort)(textureSize.X * scale), (ushort)(textureSize.Y * scale)); return(true); }
// Please note: the start position is located in bottom left corner of the layout. protected static IEnumerable <IStaticWorldObject> SharedFindObjectsNearby <TProtoObject>(Vector2Ushort startPosition) where TProtoObject : class, IProtoStaticWorldObject { var world = IsServer ? (IWorldService)Server.World : (IWorldService)Client.World; // TODO: the offset here should be taken from current prototype Layout (its center) var bounds = new RectangleInt(startPosition + (1, 1), size: (1, 1)); bounds = bounds.Inflate(MinDistanceBetweenExtractors); var objectsInBounds = world.GetStaticWorldObjectsOfProtoInBounds <TProtoObject>(bounds); return(objectsInBounds); }
private void AddMarker(Vector2Ushort position) { if (this.markers.ContainsKey(position)) { Api.Logger.Warning("Dropped items already has the map visualizer: " + position); return; } var mapControl = new WorldMapMarkDroppedItems(); var canvasPosition = this.worldMapController.WorldToCanvasPosition(position.ToVector2D()); Canvas.SetLeft(mapControl, canvasPosition.X); Canvas.SetTop(mapControl, canvasPosition.Y); Panel.SetZIndex(mapControl, 12); this.worldMapController.AddControl(mapControl); this.markers[position] = mapControl; }
private void ServerStructureRelocatedHandler( ICharacter character, Vector2Ushort fromPosition, IStaticWorldObject structure) { if (structure.ProtoGameObject != this) { return; } var bedPrivateState = GetPrivateState(structure); var currentOwner = bedPrivateState.Owner; if (currentOwner is not null) { ServerSetCurrentBed(structure, currentOwner); } }
private IStaticWorldObject ServerTryCreateGroundContainer( Vector2Ushort tilePosition, bool writeWarningsToLog = true) { if (this.CheckTileRequirements(tilePosition, character: null, logErrors: false)) { Logger.Info("Creating ground container at " + tilePosition); return(Server.World.CreateStaticWorldObject(this, tilePosition)); } if (writeWarningsToLog) { Logger.Warning( $"Cannot create ground container at {tilePosition} - tile contains something preventing it."); } return(null); }
public static void ServerSpawnDestroyedWall( Vector2Ushort tilePosition, IProtoObjectWall originalProtoObjectWall) { if (Server.World .GetTile(tilePosition) .StaticObjects .Any(so => so.ProtoStaticWorldObject is ObjectWallDestroyed)) { //Logger.Error("Already spawned a destroyed wall here: " + tilePosition); return; } var worldObject = Server.World.CreateStaticWorldObject <ObjectWallDestroyed>(tilePosition); GetPublicState(worldObject).OriginalProtoObjectWall = originalProtoObjectWall; Logger.Important($"Spawning a destroyed wall at: {tilePosition} ({originalProtoObjectWall})"); }
private static async ValueTask <Vector2Ushort> PrepareLayers( ProceduralTextureRequest request, List <ComposeLayer> spritesToCombine) { var textureDataTasks = spritesToCombine.Select( t => Rendering .GetTextureSizeWithMagentaPixelPosition(t.TextureResource)) .ToList(); foreach (var textureDataTask in textureDataTasks) { await textureDataTask; request.ThrowIfCancelled(); } var extendX = 0f; var extendY = 0f; for (var index = 0; index < textureDataTasks.Count; index++) { var result = textureDataTasks[index].Result; var pivotPos = result.MagentaPixelPosition; var composeItem = spritesToCombine[index]; composeItem.PivotPos = pivotPos; spritesToCombine[index] = composeItem; var itemExtendX = Math.Max(pivotPos.X, result.Size.X - pivotPos.X); var itemExtendY = Math.Max(pivotPos.Y, result.Size.Y - pivotPos.Y); if (itemExtendX > extendX) { extendX = itemExtendX; } if (itemExtendY > extendY) { extendY = itemExtendY; } } var resultTextureSize = new Vector2Ushort((ushort)Math.Floor(extendX * 2), (ushort)Math.Floor(extendY * 2)); return(resultTextureSize); }
public WallPattern( string name, Vector2Ushort atlasChunk, NeighborsPattern requiresNeighbors, double drawOffsetNormal = 0, double?drawOffsetDestroyed = null, Action <IPhysicsBody> physicsNormal = null, Action <IPhysicsBody> physicsDestroyed = null, bool isValidDestroyed = true) { this.Name = name; this.AtlasChunkPosition = atlasChunk; this.RequiresNeighbors = requiresNeighbors; this.DrawOffsetNormal = drawOffsetNormal; this.DrawOffsetDestroyed = drawOffsetDestroyed ?? drawOffsetNormal; this.SetupPhysicsNormal = physicsNormal; this.SetupPhysicsDestroyed = physicsDestroyed ?? physicsNormal; this.IsValidDestroyed = isValidDestroyed; }
/// <summary> /// When the explosion is processed this method will be called (see ObjectMineralPragmiumSourceExplosion). /// </summary> internal static void ServerOnExplode( Vector2Ushort epicenterPosition, double explosionRadius, ICharacter byCharacter) { // let's spawn scattered pragmium nodes around the explosion site var countToSpawnRemains = DestroySpawnNodeCount; var attemptsRemains = 2000; while (countToSpawnRemains > 0) { attemptsRemains--; if (attemptsRemains <= 0) { // attempts exceeded return; } // calculate random distance from the explosion epicenter var distance = RandomHelper.Range(2, explosionRadius); // ensure we spawn more objects closer to the epicenter var spawnProbability = 1 - (distance / explosionRadius); spawnProbability = Math.Pow(spawnProbability, 1.5); if (!RandomHelper.RollWithProbability(spawnProbability)) { // random skip continue; } var angle = RandomHelper.NextDouble() * MathConstants.DoublePI; var spawnPosition = new Vector2Ushort( (ushort)(epicenterPosition.X + distance * Math.Cos(angle)), (ushort)(epicenterPosition.Y + distance * Math.Sin(angle))); // try spawn a pragmium node if (ServerTrySpawnNode(spawnPosition, pveTagForCharacter: byCharacter)) { // spawned successfully! countToSpawnRemains--; } } }
public WorldMapControllerMiniMap( PanningPanel panningPanel, ViewModelControlWorldMap viewModelControlWorldMap, bool isPlayerMarkDisplayed, bool isCurrentCameraViewDisplayed, bool isListeningToInput, int paddingChunks, Vector2Ushort mapAreaSize, ControlTemplate customControlTemplatePlayerMark) : base(panningPanel, viewModelControlWorldMap, isPlayerMarkDisplayed, isCurrentCameraViewDisplayed, isListeningToInput, paddingChunks, customControlTemplatePlayerMark) { this.mapAreaSize = mapAreaSize; }
public static bool operator ==(CustomMarkData markData, CustomMarkData markDataK) { if (object.ReferenceEquals(markData, null)) { return(false); } if (object.ReferenceEquals(markDataK, null)) { return(false); } string[] sm = markData.Key.Split(';'); Vector2Ushort m = new Vector2Ushort(Convert.ToUInt16(sm[0]), Convert.ToUInt16(sm[1])); string[] sk = markDataK.Key.Split(';'); Vector2Ushort k = new Vector2Ushort(Convert.ToUInt16(sk[0]), Convert.ToUInt16(sk[1])); return(m.X == k.X && m.Y == k.Y); }
private void OnPlaceSelected(Vector2Ushort tilePosition, bool isButtonHeld) { if (this.delayRemainsSeconds > 0) { return; } this.validateCanBuildCallback(tilePosition, logErrors: !isButtonHeld, out var canPlace, out var isTooFar); if (!canPlace || isTooFar) { return; } this.placeSelectedCallback(tilePosition); }
private void ClientRemote_OnWeaponHitOrTrace( ICharacter firingCharacter, IProtoItemWeapon protoWeapon, IProtoItemAmmo protoAmmo, IProtoCharacter protoCharacter, Vector2Ushort fallbackCharacterPosition, WeaponHitData[] hitObjects, Vector2D endPosition, bool endsWithHit) { WeaponSystemClientDisplay.ClientOnWeaponHitOrTrace(firingCharacter, protoWeapon, protoAmmo, protoCharacter, fallbackCharacterPosition, hitObjects, endPosition, endsWithHit); }
public Sector( WorldMapSectorProvider sectorProvider, Vector2Ushort sectorWorldPosition, Vector2D sectorVisualPosition) { this.SectorWorldPosition = sectorWorldPosition; this.SectorRenderer = sectorProvider.GetSectorRenderer(sectorWorldPosition); var sectorRectangle = new Rectangle() { // + 1 is required to prevent a seam from appearing between sector rectangles Width = WorldMapSectorProvider.SectorPixelSize + 1, Height = WorldMapSectorProvider.SectorPixelSize + 1, Fill = Api.Client.UI.GetTextureBrush(this.SectorRenderer.RenderTexture) }; Canvas.SetLeft(sectorRectangle, sectorVisualPosition.X); Canvas.SetTop(sectorRectangle, sectorVisualPosition.Y - WorldMapSectorProvider.SectorPixelSize); this.SectorRectangle = sectorRectangle; }
/// <summary> /// Perform distance, height, and other checks. /// </summary> public static bool SharedIsValidStartLocation( ICharacter character, Vector2Ushort targetPosition, out bool hasObstacles) { hasObstacles = false; if (character.TilePosition.TileSqrDistanceTo(targetPosition) > DroneStartDistanceMax * DroneStartDistanceMax) { return(false); } var targetTile = IsServer ? Server.World.GetTile(targetPosition) : Client.World.GetTile(targetPosition); if (targetTile.Height != character.Tile.Height) { return(false); } // check for obstacles using var testResults = character.PhysicsBody.PhysicsSpace.TestLine( fromPosition: character.Position, toPosition: (targetPosition.X + 0.5, targetPosition.Y + 0.5), collisionGroup: CollisionGroups.Default, sendDebugEvent: false); foreach (var testResult in testResults.AsList()) { switch (testResult.PhysicsBody.AssociatedWorldObject?.ProtoGameObject) { case IProtoObjectWall _: case IProtoObjectDoor _: // don't allow mining through walls and doors hasObstacles = true; return(false); } } return(true); }
private void ClientRemote_OnWeaponShot( ICharacter whoFires, uint partyId, IProtoItemWeapon protoWeapon, IProtoCharacter fallbackProtoCharacter, Vector2Ushort fallbackPosition) { if (whoFires != null && !whoFires.IsInitialized) { whoFires = null; } WeaponSystemClientDisplay.ClientOnWeaponShot(whoFires, partyId, protoWeapon, fallbackProtoCharacter, fallbackPosition); }
private ItemExplosiveRequest ClientTryCreateRequest(ICharacter character, Vector2Ushort targetPosition) { var item = character.SharedGetPlayerSelectedHotbarItem(); if (!(item.ProtoItem is IProtoItemExplosive protoItemExplosive)) { // no explosive item selected return(null); } if (!protoItemExplosive.SharedValidatePlacement(character, targetPosition, logErrors: true)) { return(null); } return(new ItemExplosiveRequest(character, item, targetPosition)); }
public static void ClientPaste(Vector2Ushort tilePosition) { if (lastBufferEntry is null) { return; } var bufferEntry = lastBufferEntry.Value; if (ClientEditorAreaSelectorHelper.Instance is not null) { NotificationSystem.ClientShowNotification( title: null, message: "You're already in object placement mode." + ObjectPlacementGuide, color: NotificationColor.Neutral); return; } NotificationSystem.ClientShowNotification( title: null, message: $"{bufferEntry.Entries.Count} tiles ready for paste!" + ObjectPlacementGuide, color: NotificationColor.Good); var originalSize = bufferEntry.Size; // ReSharper disable once ObjectCreationAsStatement new ClientEditorAreaSelectorHelper(tilePosition, originalSize, selectedCallback: PlaceSelectedCallback); void PlaceSelectedCallback(Vector2Ushort selectedTilePosition) { var entries = bufferEntry.Entries .Select(e => e.ApplyOffset(selectedTilePosition)) .ToList(); TerrainEditingSystem.ClientModifyTerrain(entries); } }
private static void SetupBoundsForLandClaimsInScope( IClientSceneObject sceneObject, Vector2D sceneObjectPosition, Vector2Ushort originTilePosition, RectangleInt originBounds, IProtoObjectLandClaim originProtoObjectLandClaim) { var landClaims = Api.Client.World.GetStaticWorldObjectsOfProto <IProtoObjectLandClaim>(); foreach (var landClaim in landClaims) { var protoObjectLandClaim = (IProtoObjectLandClaim)landClaim.ProtoGameObject; var landClaimCenterPosition = LandClaimSystem .SharedCalculateLandClaimObjectCenterTilePosition( landClaim.TilePosition, protoObjectLandClaim); var landClaimBounds = LandClaimSystem.SharedCalculateLandClaimAreaBounds( landClaimCenterPosition, protoObjectLandClaim.LandClaimWithGraceAreaSize); var intersectionDepth = CalculateIntersectionDepth(originBounds, landClaimBounds); if (intersectionDepth < 0) { // no intersection continue; } intersectionDepth = (intersectionDepth + 1) / 2; intersectionDepth = Math.Min(intersectionDepth, originProtoObjectLandClaim.LandClaimGraceAreaPaddingSizeOneDirection + 1); var exceptBounds = originBounds.Inflate(-intersectionDepth); using var tempList = Api.Shared.WrapObjectInTempList(exceptBounds); AddBoundLabels(sceneObject, sceneObjectPosition, exceptBounds: tempList.AsList(), protoObjectLandClaim, positionOffset: landClaimCenterPosition.ToVector2D() - originTilePosition.ToVector2D()); } }
private void ClientRemote_OnStructurePlaced( IProtoStaticWorldObject protoStaticWorldObject, Vector2Ushort position, bool isByCurrentPlayer) { var soundPreset = protoStaticWorldObject.SharedGetObjectSoundPreset(); if (isByCurrentPlayer) { // play 2D sound soundPreset.PlaySound(ObjectSound.Place, limitOnePerFrame: false); } else { // play 3D sound (at the built object location) soundPreset.PlaySound(ObjectSound.Place, position.ToVector2D() + protoStaticWorldObject.Layout.Center); } }
/// <summary> /// Server spawn callback for mob. /// </summary> /// <param name="trigger">Trigger leading to this spawn.</param> /// <param name="zone">Server zone instance.</param> /// <param name="protoMob">Prototype of character mob object to spawn.</param> /// <param name="tilePosition">Position to try spawn at.</param> protected virtual IGameObjectWithProto ServerSpawnMob( IProtoTrigger trigger, IServerZone zone, IProtoCharacterMob protoMob, Vector2Ushort tilePosition) { var worldPosition = tilePosition.ToVector2D(); if (!ServerCharacterSpawnHelper.IsPositionValidForCharacterSpawn(worldPosition, isPlayer: false)) { // position is not valid for spawning return(null); } return(Server.Characters.SpawnCharacter( protoMob, worldPosition)); }
public static RectangleInt SharedCalculateLandClaimAreaBounds(Vector2Ushort centerTilePosition, ushort size) { var worldBounds = IsServer ? ServerWorld.WorldBounds : ClientWorld.WorldBounds; var pos = centerTilePosition; var halfSize = size / 2.0; var start = new Vector2Ushort( (ushort)Math.Max(Math.Ceiling(pos.X - halfSize), worldBounds.MinX), (ushort)Math.Max(Math.Ceiling(pos.Y - halfSize), worldBounds.MinY)); var endX = Math.Min(Math.Ceiling(pos.X + halfSize), worldBounds.MaxX); var endY = Math.Min(Math.Ceiling(pos.Y + halfSize), worldBounds.MaxY); var calculatedSize = new Vector2Ushort((ushort)(endX - start.X), (ushort)(endY - start.Y)); return(new RectangleInt(start, size: calculatedSize)); }
private bool CheckIsValidOffset(Vector2Ushort offset) { var worldBounds = Api.Client.World.WorldBounds; var maxPosition = offset + this.size; if (maxPosition.X < worldBounds.MaxX && maxPosition.Y < worldBounds.MaxY && offset.X > worldBounds.MinX && offset.Y > worldBounds.MinY) { return(true); } NotificationSystem.ClientShowNotification( title: null, message: "Out of world bounds, please select different location", color: NotificationColor.Bad); return(false); }
private static GetPlantResult SharedGetAvailablePlantAt(Vector2Ushort tilePosition, ICharacter character) { var tile = WorldService.GetTile(tilePosition); var objectPlant = tile.StaticObjects.FirstOrDefault(so => so.ProtoStaticWorldObject is IProtoObjectPlant); if (objectPlant is null) { return GetPlantResult.Fail(CannotApplyErrorTitle, "Can apply only on plants."); } if (!objectPlant.ProtoStaticWorldObject .SharedCanInteract( character, objectPlant, writeToLog: false)) { return GetPlantResult.Fail(CoreStrings.Notification_TooFar, "Come closer to apply fertilizer."); } return GetPlantResult.Success(objectPlant); }