/// <summary> /// Method to determine resources required for this activity in the current month /// </summary> /// <returns>List of required resource requests</returns> public override List <ResourceRequest> GetResourcesNeededForActivity() { if (people is null | food is null) { return(null); } List <LabourType> peopleList = people.Items.Where(a => IncludeHiredLabour || a.Hired == false).ToList(); peopleList.Select(a => a.FeedToTargetIntake == 0); // determine AEs to be fed double aE = peopleList.Sum(a => a.AdultEquivalent); if (aE <= 0) { return(null); } int daysInMonth = DateTime.DaysInMonth(Clock.Today.Year, Clock.Today.Month); // determine feed limits (max kg per AE per day * AEs * days) double intakeLimit = DailyIntakeLimit * aE * daysInMonth; // remove previous consumption double otherIntake = this.DailyIntakeOtherSources * aE * daysInMonth; otherIntake += peopleList.Sum(a => a.GetAmountConsumed()); List <LabourActivityFeedTarget> labourActivityFeedTargets = this.FindAllChildren <LabourActivityFeedTarget>().Cast <LabourActivityFeedTarget>().ToList(); // determine targets foreach (LabourActivityFeedTarget target in labourActivityFeedTargets) { // calculate target target.Target = target.TargetValue * aE * daysInMonth; // set initial level based on off store inputs target.CurrentAchieved = target.OtherSourcesValue * aE * daysInMonth; // calculate current level from previous intake this month (LabourActivityFeed) target.CurrentAchieved += people.GetDietaryValue(target.Metric, IncludeHiredLabour, true) * aE * daysInMonth; // add sources outside of this activity to peoples' diets if (target.OtherSourcesValue > 0) { foreach (var person in peopleList) { LabourDietComponent outsideEat = new LabourDietComponent(); outsideEat.AddOtherSource(target.Metric, target.OtherSourcesValue * person.AdultEquivalent * daysInMonth); person.AddIntake(outsideEat); } } } // get max months before spoiling of all food stored (will be zero for non perishable food) int maxFoodAge = food.FindAllChildren <HumanFoodStoreType>().Cast <HumanFoodStoreType>().Max(a => a.Pools.Select(b => a.UseByAge - b.Age).DefaultIfEmpty(0).Max()); // create list of all food parcels List <HumanFoodParcel> foodParcels = new List <HumanFoodParcel>(); foreach (HumanFoodStoreType foodStore in food.FindAllChildren <HumanFoodStoreType>().Cast <HumanFoodStoreType>().ToList()) { foreach (HumanFoodStorePool pool in foodStore.Pools) { foodParcels.Add(new HumanFoodParcel() { FoodStore = foodStore, Pool = pool, Expires = ((foodStore.UseByAge == 0) ? maxFoodAge + 1: foodStore.UseByAge - pool.Age) }); } } foodParcels = foodParcels.OrderBy(a => a.Expires).ToList(); // if a market exists add the available market produce to the list below that ordered above. // order market food by price ascending // this will include market available food in the decisions. // will need to purchase this food before taking it if cost associated. // We can check if the parent of the human food store used is a market and charge accordingly. // for each market List <HumanFoodParcel> marketFoodParcels = new List <HumanFoodParcel>(); ResourcesHolder resources = Resources.FoundMarket.Resources; if (resources != null) { HumanFoodStore food = resources.HumanFoodStore(); if (food != null) { foreach (HumanFoodStoreType foodStore in food.FindAllChildren <HumanFoodStoreType>()) { foreach (HumanFoodStorePool pool in foodStore.Pools) { marketFoodParcels.Add(new HumanFoodParcel() { FoodStore = foodStore, Pool = pool, Expires = ((foodStore.UseByAge == 0) ? maxFoodAge + 1 : foodStore.UseByAge - pool.Age) }); } } } } foodParcels.AddRange(marketFoodParcels.OrderBy(a => a.FoodStore.Price(PurchaseOrSalePricingStyleType.Purchase).PricePerPacket)); double fundsAvailable = double.PositiveInfinity; if (bankAccount != null) { fundsAvailable = bankAccount.FundsAvailable; } int parcelIndex = 0; double intake = otherIntake; // start eating food from list from that about to expire first while (parcelIndex < foodParcels.Count) { foodParcels[parcelIndex].Proportion = 0; if (intake < intakeLimit & (labourActivityFeedTargets.Where(a => !a.TargetMet).Count() > 0 | foodParcels[parcelIndex].Expires == 0)) { // still able to eat and target not met or food about to expire this timestep // reduce by amout that can be eaten double propCanBeEaten = Math.Min(1, (intakeLimit - intake) / (foodParcels[parcelIndex].FoodStore.EdibleProportion * foodParcels[parcelIndex].Pool.Amount)); // reduce to target limits double propToTarget = 1; if (foodParcels[parcelIndex].Expires != 0) { // if the food is not going to spoil // then adjust what can be eaten up to target otherwise allow over target consumption to avoid waste LabourActivityFeedTarget targetUnfilled = labourActivityFeedTargets.Where(a => !a.TargetMet).FirstOrDefault(); if (targetUnfilled != null) { // calculate reduction to metric target double metricneeded = Math.Max(0, targetUnfilled.Target - targetUnfilled.CurrentAchieved); double amountneeded = metricneeded / foodParcels[parcelIndex].FoodStore.ConversionFactor(targetUnfilled.Metric); propToTarget = Math.Min(1, amountneeded / (foodParcels[parcelIndex].FoodStore.EdibleProportion * foodParcels[parcelIndex].Pool.Amount)); } } foodParcels[parcelIndex].Proportion = Math.Min(propCanBeEaten, propToTarget); // work out if there will be a cost limitation, only if a price structure exists for the resource double propToPrice = 1; if (foodParcels[parcelIndex].FoodStore.PricingExists(PurchaseOrSalePricingStyleType.Purchase)) { ResourcePricing price = foodParcels[parcelIndex].FoodStore.Price(PurchaseOrSalePricingStyleType.Purchase); double cost = (foodParcels[parcelIndex].Pool.Amount * foodParcels[parcelIndex].Proportion) / price.PacketSize * price.PricePerPacket; if (cost > 0) { propToPrice = Math.Min(1, fundsAvailable / cost); // remove cost from running check tally fundsAvailable = Math.Max(0, fundsAvailable - (cost * propToPrice)); // real finance transactions will happen in the do activity as stuff is allocated // there should not be shortfall as all the checks and reductions have happened here } } foodParcels[parcelIndex].Proportion *= propToPrice; // update intake double newIntake = (foodParcels[parcelIndex].FoodStore.EdibleProportion * foodParcels[parcelIndex].Pool.Amount * foodParcels[parcelIndex].Proportion); intake += newIntake; // update metrics foreach (LabourActivityFeedTarget target in labourActivityFeedTargets) { target.CurrentAchieved += newIntake * foodParcels[parcelIndex].FoodStore.ConversionFactor(target.Metric); } } else if (intake >= intakeLimit && labourActivityFeedTargets.Where(a => !a.TargetMet).Count() > 1) { // full but could still reach target with some substitution // but can substitute to remove a previous target // does the current parcel have better target values than any previous non age 0 pool of a different food type } else { break; } parcelIndex++; } // fill resource requests List <ResourceRequest> requests = new List <ResourceRequest>(); foreach (var item in foodParcels.GroupBy(a => a.FoodStore)) { double amount = item.Sum(a => a.Pool.Amount * a.Proportion); if (amount > 0) { double financeLimit = 1; // if obtained from the market make financial transaction before taking ResourcePricing price = item.Key.Price(PurchaseOrSalePricingStyleType.Sale); if (bankAccount != null && item.Key.Parent.Parent.Parent == Market && price.PricePerPacket > 0) { // if shortfall reduce purchase ResourceRequest marketRequest = new ResourceRequest { ActivityModel = this, Required = amount / price.PacketSize * price.PricePerPacket, AllowTransmutation = false, Category = "Food purchase", MarketTransactionMultiplier = 1 }; bankAccount.Remove(marketRequest); } requests.Add(new ResourceRequest() { Resource = item.Key, ResourceType = typeof(HumanFoodStore), AllowTransmutation = false, Required = amount * financeLimit, ResourceTypeName = item.Key.Name, ActivityModel = this, Category = "Consumption" }); } } // if still hungry and funds available, try buy food in excess of what stores (private or market) offered using transmutation if present. // This will force the market or private sources to purchase more food to meet demand if transmutation available. // if no market is present it will look to transmutating from its own stores if possible. // this means that other than a purchase from market (above) this activity doesn't need to worry about financial tranactions. if (intake < intakeLimit && (labourActivityFeedTargets.Where(a => !a.TargetMet).Count() > 0) && fundsAvailable > 0) { ResourcesHolder resourcesHolder = Resources; // if market is present point to market to find the resource if (Market != null) { resourcesHolder = Market.FindChild <ResourcesHolder>(); } // don't worry about money anymore. The over request will be handled by the transmutation. // move through specified purchase list foreach (LabourActivityFeedTargetPurchase purchase in this.FindAllChildren <LabourActivityFeedTargetPurchase>().Cast <LabourActivityFeedTargetPurchase>().ToList()) { HumanFoodStoreType foodtype = resourcesHolder.GetResourceItem(this, purchase.FoodStoreName, OnMissingResourceActionTypes.Ignore, OnMissingResourceActionTypes.Ignore) as HumanFoodStoreType; if (foodtype != null && (foodtype.TransmutationDefined & intake < intakeLimit)) { LabourActivityFeedTarget targetUnfilled = labourActivityFeedTargets.Where(a => !a.TargetMet).FirstOrDefault(); if (targetUnfilled != null) { // calculate reduction to metric target double metricneeded = Math.Max(0, (targetUnfilled.Target - targetUnfilled.CurrentAchieved)); double amountneeded = metricneeded / foodtype.ConversionFactor(targetUnfilled.Metric); if (intake + amountneeded > intakeLimit) { amountneeded = intakeLimit - intake; } double amountfood = amountneeded / foodtype.EdibleProportion; // update intake intake += amountfood; // find in requests or create a new one ResourceRequest foodRequestFound = requests.Find(a => a.Resource == foodtype) as ResourceRequest; if (foodRequestFound is null) { requests.Add(new ResourceRequest() { Resource = foodtype, ResourceType = typeof(HumanFoodStore), AllowTransmutation = true, Required = amountfood, ResourceTypeName = purchase.FoodStoreName.Split('.')[1], ActivityModel = this, Category = "Consumption" }); } else { foodRequestFound.Required += amountneeded; foodRequestFound.AllowTransmutation = true; } } } } // NOTE: proportions of purchased food are not modified if the sum does not add up to 1 or some of the food types are not available. } return(requests); }