protected Cropbeast(CropTile cropTile, bool containsPlant, bool containsPrimaryHarvest, bool containsSecondaryHarvest, string beastName = null) : base(beastName ?? cropTile.mapping.beastName, cropTile.location, Utility.PointToVector2(cropTile.tileLocation) * 64f + new Vector2(0f, -32f)) { cropTile_ = cropTile; harvestIndex.Value = cropTile.harvestIndex; giantCrop.Value = cropTile.giantCrop; tileLocation.Value = cropTile.tileLocation; this.containsPlant.Value = containsPlant; this.containsPrimaryHarvest.Value = containsPrimaryHarvest; this.containsSecondaryHarvest.Value = containsSecondaryHarvest; // Calculate the beast's coloration. primaryColor.Value = cropTile.mapping.primaryColor ?? TailoringMenu.GetDyeColor(cropTile.mapping.harvestObject) ?? Color.White; secondaryColor.Value = cropTile.mapping.secondaryColor ?? primaryColor.Value; // Scale health and damage based on combat skill of players present. // Base values in Monsters.json are for level zero. MaxHealth = Health = scaleByCombatSkill(Health, 1.2f); DamageToFarmer = scaleByCombatSkill(DamageToFarmer, 0.2f); // Calculate the normal gain of farming experience. int price = cropTile.mapping.harvestObject.Price; experienceGained.Value = (int)Math.Ceiling(8.0 * Math.Log(0.018 * (double)price + 1.0, Math.E)); }
internal bool spawnCropbeast(bool console = false, bool force = false, string filter = null) { // Check preconditions. if (!Context.IsWorldReady) { throw new Exception("The world is not ready."); } if (!Context.IsMainPlayer) { throw new InvalidOperationException("Only the host can do that."); } // Choose a crop tile. CropTile tile = chooser.choose(console: console, force: force, filter: filter); if (tile == null) { return(false); } // Spawn the cropbeast. Spawner.Spawn(tile); return(true); }
public static void Spawn(CropTile cropTile) { // Check preconditions. if (!Context.IsMainPlayer) { throw new InvalidOperationException("Only the host can do that."); } // Find the nature of the beast. ConstructorInfo ctor = cropTile.mapping.type?.GetConstructor (new Type[] { typeof(CropTile), typeof(bool) }); if (ctor == null) { throw new Exception($"Invalid cropbeast type '{cropTile.mapping.beastName}'."); } // Create the beast(s). List <Cropbeast> beasts = new List <Cropbeast> (); beasts.Add(ctor.Invoke(new object[] { cropTile, true }) as Cropbeast); if (cropTile.baseQuantity > 1 && !cropTile.giantCrop) { for (int i = 1; i < cropTile.baseQuantity; ++i) { beasts.Add(ctor.Invoke(new object[] { cropTile, false }) as Cropbeast); } } // Register the beast(s) centrally in advance. foreach (Cropbeast beast in beasts) { ModEntry.Instance.registerMonster(beast); } // Have the witch fly by only if configured, only outdoors and only // on the first real spawn of the day. bool showWitchFlyover = Config.WitchFlyovers && cropTile.location.IsOutdoors && !cropTile.fake && !ModEntry.Instance.chooser.witchFlyoverShown; // Create the host spawner here. new Spawner(cropTile, cropTile.location, cropTile.baseQuantity, beasts, showWitchFlyover); // Signal farmhands to create guest spawners. Message message = new Message { locationName = cropTile.location.Name, tileLocation = cropTile.tileLocation, harvestIndex = cropTile.harvestIndex, giantCrop = cropTile.giantCrop, baseQuantity = cropTile.baseQuantity, showWitchFlyover = showWitchFlyover, }; Helper.Multiplayer.SendMessage(message, "CreateSpawner", modIDs: new string[] { ModEntry.Instance.ModManifest.UniqueID }); }
private static void fakeOne(int harvestIndex, bool giantCrop, Farmer near = null) { near ??= Game1.player; GameLocation location = near.currentLocation; var tiles = Utility.recursiveFindOpenTiles(location, near.getTileLocation(), 1, 100); if (tiles.Count == 0) { throw new Exception($"Could not find an open tile in {location.Name}."); } Vector2 tileLocation = tiles[0]; Crop crop = Utilities.MakeNonceCrop(harvestIndex, Utility.Vector2ToPoint(tileLocation)); TerrainFeature feature; if (giantCrop) { if (location is Farm farm) { feature = new GiantCrop(harvestIndex, tileLocation); farm.resourceClumps.Add(feature as ResourceClump); } else { throw new Exception($"Cannot fake a Giant Cropbeast in {location.Name}."); } } else { feature = new HoeDirt(0, crop); location.terrainFeatures.Add(tileLocation, feature); } CropTile cropTile = new CropTile(feature, crop, giantCrop, location, Utility.Vector2ToPoint(tileLocation), fake: true); if (cropTile.state != CropTile.State.Available) { cropTile.cleanUpFake(); throw new Exception($"Faked a {cropTile.logDescription} but it had no available cropbeast mapping."); } cropTile.choose(); DelayedAction.functionAfterDelay(() => Spawner.Spawn(cropTile), 0); }
// Alters records as if the given crop tile had never been chosen. public virtual void unchoose(CropTile tile) { if (!chosenTiles.ContainsKey(tile.tileLocation)) { return; } if (tile.location.IsOutdoors) { --outdoorSpawnCount; } else { --indoorSpawnCount; } chosenTiles.Remove(tile.tileLocation); }
public virtual void revert() { if (reverted) { return; } CropTile cropTile = cropTile_ as CropTile; if (cropTile == null) { throw new Exception("Cannot revert cropbeast without true CropTile."); } reverted = true; Monitor.Log($"Reverting a {Name} to a {cropTile.logDescription}.", LogLevel.Debug); if (containsPlant.Value) { ModEntry.Instance.chooser?.unchoose(cropTile); } if (containsPrimaryHarvest.Value) { cropTile.restoreFully(); } else if (containsPlant.Value && cropTile.repeatingCrop) { cropTile.restorePlant(); } if (currentLocation != null) { currentLocation.characters.Remove(this); } Health = -500; }
// Chooses a crop tile to become a cropbeast. public virtual CropTile choose(bool console = false, bool force = false, string filter = null) { // Use a predictable RNG seeded by game, day and time. Random rng = new Random((int)Game1.uniqueIDForThisGame / 2 + (int)Game1.stats.DaysPlayed + Game1.timeOfDay); // Choose the location to evaluate. GameLocation location = Game1.currentLocation; if (!(location.IsFarm && location.IsOutdoors) && !location.IsGreenhouse) { GameLocation farm = Game1.getLocationFromName("Farm"); GameLocation greenhouse = Game1.getLocationFromName("Greenhouse"); if (farm != null && farm.farmers.Count() > 0) { location = farm; } else if (greenhouse != null && greenhouse.farmers.Count() > 0) { location = greenhouse; } } // Check preconditions unless forced. if (!force && !shouldChoose(rng, location, console: console)) { return(null); } // Get the maximum distance on the current map for weighting use. var mapLayer = location.map.Layers[0]; float mapDiagonal = Vector2.Distance(new Vector2(0, 0), new Vector2(mapLayer.LayerWidth, mapLayer.LayerHeight)); // Find all candidate crop tiles, sort pseudorandomly with // weighting based on type and towards those closer to a farmer. SortedList <double, CropTile> candidates = new SortedList <double, CropTile> (findCandidateCropTiles(location, filter, console) .ToDictionary((tile) => { Utilities.FindNearestFarmer(tile.location, tile.tileLocation, out float distance); return(rng.NextDouble() * 1.5 - tile.mapping.choiceWeight + distance / mapDiagonal * 2.0); })); // If the list is empty, give up. if (candidates.Count == 0) { Monitor.Log($"At {Game1.getTimeOfDayString (Game1.timeOfDay)}, {location.Name} met preconditions but had no candidate crops to become cropbeasts.", console ? LogLevel.Warn : LogLevel.Trace); return(null); } // Choose the top of the list as the winner. CropTile winner = candidates.First().Value; Monitor.Log($"At {Game1.getTimeOfDayString (Game1.timeOfDay)}, chose {winner.logDescription} to become {winner.mapping.beastName}.", console ? LogLevel.Info : LogLevel.Debug); // Record progress towards daily limit for location. if (location.IsOutdoors) { ++outdoorSpawnCount; } else { ++indoorSpawnCount; } // Make sure this tile isn't chosen again. chosenTiles[winner.tileLocation] = winner; winner.choose(); return(winner); }
// Lists all the tiles in the given location with crops // on them that are candidates for becoming cropbeasts. protected virtual List <CropTile> findCandidateCropTiles(GameLocation location, string filter = null, bool console = false) { List <SObject> wickedStatues = Utilities.FindWickedStatues(location); List <JunimoHut> junimoHuts = Utilities.FindActiveJunimoHuts(location); // For IF2R, avoid cropbeast-spawning special giant crops in an area // of the map that is initially inaccessible. List <Point> exemptTiles = new List <Point> (); if (location is Farm && Game1.whichFarm == Farm.default_layout && Helper.ModRegistry.IsLoaded("flashshifter.immersivefarm2remastered")) { exemptTiles.Add(new Point(79, 99)); } return(CropTile.FindAll(location).Where((tile) => { // The crop must be available. if (tile.state != CropTile.State.Available) { return false; } // If a filter was supplied, it must match. if (filter != null && !tile.mapping.matchesFilter(filter)) { if (console) { Monitor.Log($"Excluded a {tile.logDescription} because it does not match the filter \"{filter}\"."); } return false; } // The crop's tile must not be on the exempt list for the map. Point tileLoc = tile.tileLocation; if (exemptTiles.Contains(tileLoc)) { if (console) { Monitor.Log($"Excluded a {tile.logDescription} because its tile is exempt on this map."); } return false; } // The crop must not have already been chosen by another // in-progress cropbeast spawn. if (chosenTiles.ContainsKey(tileLoc)) { if (console) { Monitor.Log($"Excluded a {tile.logDescription} because its tile has already been chosen."); } return false; } // A non-giant crop must not be in range of an active Junimo Hut. if (!tile.giantCrop) { foreach (JunimoHut junimoHut in junimoHuts) { if (tileLoc.X >= junimoHut.tileX.Value + 1 - 8 && tileLoc.X < junimoHut.tileX.Value + 2 + 8 && tileLoc.Y >= junimoHut.tileY.Value - 8 + 1 && tileLoc.Y < junimoHut.tileY.Value + 2 + 8) { if (console) { Monitor.Log($"Excluded a {tile.logDescription} because it is in range of an active Junimo Hut at ({junimoHut.tileX.Value},{junimoHut.tileY.Value})."); } return false; } } } // The crop must not be in range of a Wicked Statue. foreach (SObject wickedStatue in wickedStatues) { if (Config.WickedStatueRange == -1 || // infinite range Vector2.Distance(Utility.PointToVector2(tileLoc), wickedStatue.TileLocation) < (float)Config.WickedStatueRange) { if (console) { Monitor.Log($"Excluded a {tile.logDescription} because it is in range of a Wicked Statue at ({(int) wickedStatue.TileLocation.X},{(int) wickedStatue.TileLocation.Y})."); } return false; } } return true; }).ToList()); }
// Checks whether an attempt to spawn a cropbeast should be made now. protected virtual bool shouldChoose(Random rng, GameLocation location, bool console = false) { // Only spawn in daytime. Nighttime is for the regular Wilderness // Farm monsters. The dusk hour is left as a buffer period. if (Game1.timeOfDay >= 1800) { if (console) { Monitor.Log($"Not spawning a cropbeast because the current time {Game1.timeOfDay} is after 1800.", LogLevel.Info); } return(false); } // Don't spawn on a festival or wedding day. if (Utility.isFestivalDay(Game1.dayOfMonth, Game1.currentSeason) || Game1.weddingToday) { if (console) { Monitor.Log($"Not spawning a cropbeast because it is a festival or wedding day.", LogLevel.Info); } return(false); } // Only spawn if all previous cropbeasts have been slain, // unless otherwise configured. if (ModEntry.Instance.currentBeastCount > 0 && !Config.AllowSimultaneous) { if (console) { Monitor.Log($"Not spawning a cropbeast because there are already {ModEntry.Instance.currentBeastCount} active and we are not configured for simultaneous spawns.", LogLevel.Info); } return(false); } // Only spawn on farms with monsters, unless otherwise configured. if (!Game1.spawnMonstersAtNight && !Config.SpawnOnAnyFarm) { if (console) { Monitor.Log($"Not spawning a cropbeast because farm monsters are deactivated and we are not configured to ignore that.", LogLevel.Info); } return(false); } // Only spawn outdoors on the farm or in the greenhouse. if (!(location.IsFarm && location.IsOutdoors) && !location.IsGreenhouse) { if (console) { Monitor.Log($"Not spawning a cropbeast because {location.Name} is not on outdoors on the farm or in the greenhouse.", LogLevel.Info); } return(false); } bool outdoors = location.IsOutdoors; // Only spawn outdoors if there are at least 16 crops. int totalCount = CropTile.CountAll(location); if (outdoors && totalCount < 16) { if (console) { Monitor.Log($"Not spawning a cropbeast because there are only {totalCount} crop(s) on {location.Name}.", LogLevel.Info); } return(false); } // Only spawn if the applicable daily limit has not been hit. int limit = outdoors ? Config.OutdoorSpawnLimit : Config.IndoorSpawnLimit; int count = outdoors ? outdoorSpawnCount : indoorSpawnCount; if (limit >= 0 && count >= limit) { if (console) { Monitor.Log($"Not spawning a cropbeast because we have already spawned {count} out of a limit of {limit} {(outdoors ? "outdoors" : "indoors")}.", LogLevel.Info); } return(false); } // Give a small fixed chance of a cropbeast spawning at each clock // tick, scaled based on the total number of spawns expected so that // the desired limit fits within the day comfortably. double chance = 0.01 * limit; // However, don't let too much time pass without a spawn lest the // count fall too far short of the desired limit. double dayProgress = (Game1.timeOfDay / 100 - 6) / 12.0; double spawnProgress = (count + 1) / (double)limit; if (dayProgress > spawnProgress) { chance *= 5.0; } // Roll the die. Always succeed for no RNG or the console command. if (rng == null) { return(true); } else if (rng.NextDouble() < chance) { return(true); } else if (console) { Monitor.Log($"If not by console command, wouldn't have spawned a cropbeast due to the chance roll.", LogLevel.Info); return(true); } else { return(false); } }
protected virtual void calculateHarvest(bool recalculate = false) { // This method is based on GiantCrop.performToolAction and // Crop.harvest except as noted. Since the RNG is called in a // different order, the results will not match the specific original // crop, but should match the stock algorithm on average. if (calculatedHarvest && !recalculate) { return; } calculatedHarvest = true; // This can only be run from the host. CropTile cropTile = cropTile_ as CropTile; if (cropTile == null) { throw new Exception("Cannot calculate harvest without true CropTile."); } // Start with nothing. primaryHarvest = null; secondaryHarvest = null; tertiaryHarvest = null; // Chance of a quality bump for skilful combat, even to iridium. int qualityBonus = (cropTile.rng.NextDouble() < rateCombatPerformance()) ? 1 : 0; // This should never happen. if (cropTile.harvestIndex <= 0) { return; } // Calculate any programmatic coloration of the harvest. Color?tintColor = null; if (cropTile.crop.programColored.Value) { tintColor = cropTile.crop.tintColor.Value; } // Secondary harvests always have regular quality. if (containsSecondaryHarvest.Value) { int secondaryQuantity = (cropTile.giantCrop && containsPrimaryHarvest.Value) ? cropTile.baseQuantity - 1 : 1; secondaryHarvest = tintColor.HasValue ? new ColoredObject(cropTile.harvestIndex, secondaryQuantity, tintColor.Value) : new SObject(cropTile.harvestIndex, secondaryQuantity); } // Primary harvest goes with tertiary harvest and special drops. if (!containsPrimaryHarvest.Value) { objectsToDrop.Clear(); return; } // Add the primary harvest with the determined quality. int quality = cropTile.giantCrop ? qualityBonus * 2 // regular or gold : cropTile.baseQuality + qualityBonus; if (quality == 3) { quality = 4; } primaryHarvest = tintColor.HasValue ? new ColoredObject(cropTile.harvestIndex, 1, tintColor.Value) { Quality = quality } : new SObject(cropTile.harvestIndex, 1, quality: quality); // Add any tertiary harvest. The special seed drop for Sunflowers is // omitted here since they do not become cropbeasts by default. // Wheat sometimes drops Hay. if (cropTile.harvestIndex == 262 && cropTile.rng.NextDouble() < 0.4) { tertiaryHarvest = new SObject(178, 1); } // Otherwise, cropbeasts rarely drop more seeds. Does not apply // to Coffee Beans (which are themselves seeds). Give between one // and five seeds, but not more than twice the harvest's worth. else if (cropTile.harvestIndex != cropTile.seedIndex && cropTile.rng.NextDouble() < 0.05) { tertiaryHarvest = new SObject(cropTile.seedIndex, 1); tertiaryHarvest.Stack = cropTile.rng.Next(1, Math.Min(6, 1 + (2 * primaryHarvest.Price * cropTile.baseQuantity) / Math.Max(10, tertiaryHarvest.Price))); } }