From 6779dbca471ed1cce01a6954107f416a1f165caa Mon Sep 17 00:00:00 2001 From: nbintertech Date: Thu, 14 Mar 2024 08:53:52 -0500 Subject: [PATCH] Add EOG projections, fix year recap projections --- GAMEPLAY.md | 4 +- src/Financing.tsx | 34 +++++- src/Projects.tsx | 15 ++- src/components/CurrentPage.tsx | 9 +- .../EndGameReport/EndGameReportPage.tsx | 100 +++++++++++++++++- src/components/EnergyUseLineChart.tsx | 2 - src/components/YearRecap.tsx | 47 +------- src/trackedStats.tsx | 11 ++ 8 files changed, 160 insertions(+), 62 deletions(-) diff --git a/GAMEPLAY.md b/GAMEPLAY.md index 536d92a..c2f9ad4 100644 --- a/GAMEPLAY.md +++ b/GAMEPLAY.md @@ -53,14 +53,14 @@ - Mid-Sized Solar, Community Wind, Utility-PPPA Wind ###### One-time Payment Renewables: - Are paid for once, regardless of 1 or 2 year gameplay, and are renewed each year + Can be paid for once (or financed), regardless of 1 or 2 year gameplay, and are renewed each year - Solar Panels Carport, Rooftop mid-sized Solar ###### Power Purchase Agreements (PPA): Are paid for annually over a 10-year term. This is a special payment type and should not be included in most of the gameplay mechanisms and logic related to financing. ###### Always Carryover cost Savings: - Regardless of game settings, these projects carryover cost savings every year + **!!!!** Regardless of game settings, these projects carryover cost savings every year - Small solar carport, Rooftop mid-sized Solar, Community Wind diff --git a/src/Financing.tsx b/src/Financing.tsx index f86a831..70a7e7d 100644 --- a/src/Financing.tsx +++ b/src/Financing.tsx @@ -1,4 +1,4 @@ -import type { ImplementedProject, ProjectControl, RecapSurprise } from './ProjectControl'; +import type { ImplementedProject, ProjectControl, RecapSurprise, RenewableProject } from './ProjectControl'; import Projects from './Projects'; import type { TrackedStats } from './trackedStats'; import AttachMoneyIcon from '@mui/icons-material/AttachMoney'; @@ -144,21 +144,53 @@ export function getRoundIsExpired(round: FundingRound, stats: TrackedStats) { export function getDefaultFinancingOption(projectControl: ProjectControl, hasFinancingOptions: boolean, baseCost: number): FinancingOption { let name = 'Pay with Existing Budget'; let description = hasFinancingOptions ? 'Reduce energy use with a one-time payment' : 'Pay for project with funds from current budget'; + let term; if (projectControl.customBudgetType) { name = projectControl.customBudgetType.name; description = projectControl.customBudgetType.description; + term = projectControl.customBudgetType.loanTerm; } return { financingType: { name: name, description: description, id: 'budget', + loanTerm: term }, financedTotalCost: baseCost, financedAnnualCost: undefined, } } +export function getProjectedFinancedSpending(implementedFinancedProjects: ImplementedProject[], renewableProjects: RenewableProject[], mutableStats: TrackedStats) { + let futureFinancedSpending: number = 0; + futureFinancedSpending = implementedFinancedProjects.reduce((totalFutureSpending: number, project: ImplementedProject) => { + return getRemainingProjectCosts(project, mutableStats, totalFutureSpending); + }, 0); + futureFinancedSpending += renewableProjects.reduce((totalFutureSpending: number, project: ImplementedProject) => { + return getRemainingProjectCosts(project, mutableStats, totalFutureSpending); + }, 0); + return futureFinancedSpending; +} + + +export function getRemainingProjectCosts(project: ImplementedProject, mutableStats: TrackedStats, totalFutureSpending: number) { + let finishedGameYear = mutableStats.currentGameYear + 1; + let yearsPaid = finishedGameYear - project.yearStarted; + yearsPaid = yearsPaid * mutableStats.gameYearInterval + let yearsRemaining = project.financingOption.financingType.loanTerm - yearsPaid; + let projectControl = Projects[project.page]; + let isAnnuallyFinanced = getIsAnnuallyFinanced(project.financingOption.financingType.id); + if ((isAnnuallyFinanced || projectControl.isPPA) && yearsRemaining) { + // we always want this cost to be expressed in single year + let yearInterval = 1 + let annualCost = projectControl.getImplementationCost(project.financingOption.financingType.id, yearInterval); + let futureCosts = annualCost * yearsRemaining; + totalFutureSpending += futureCosts; + } + return totalFutureSpending; +} + /** * Set milestone round is earned diff --git a/src/Projects.tsx b/src/Projects.tsx index 6367819..0a4affe 100644 --- a/src/Projects.tsx +++ b/src/Projects.tsx @@ -1093,7 +1093,8 @@ Projects[Pages.smallVPPA] = new ProjectControl({ customBudgetType: { name: "Power Purchase Agreement", description: "Pay Annually", - id: 'budget' + id: 'budget', + loanTerm: 10 }, title: 'Invest in wind VPPA', shortTitle: 'Invest in wind VPPA to offset {10%} of your electricity emissions. {THIS PROJECT WILL BE RENEWED ANNUALLY}.', @@ -1126,7 +1127,8 @@ Projects[Pages.midVPPA] = new ProjectControl({ customBudgetType: { name: "Power Purchase Agreement", description: "Pay Annually", - id: 'budget' + id: 'budget', + loanTerm: 10 }, title: 'Invest in wind VPPA', shortTitle: 'Invest in wind VPPA to offset {20%} of your electricity emissions. {THIS PROJECT WILL BE RENEWED ANNUALLY}.', @@ -1159,7 +1161,8 @@ Projects[Pages.largeVPPA] = new ProjectControl({ customBudgetType: { name: "Power Purchase Agreement", description: "Pay Annually", - id: 'budget' + id: 'budget', + loanTerm: 10 }, title: 'Invest in wind VPPA', shortTitle: 'Invest in wind VPPA to offset {30%} of your electricity emissions. {THIS PROJECT WILL BE RENEWED ANNUALLY}.', @@ -1189,7 +1192,8 @@ Projects[Pages.midSolar] = new ProjectControl({ customBudgetType: { name: "Power Purchase Agreement", description: "Pay Annually", - id: 'budget' + id: 'budget', + loanTerm: 10 }, statsInfoAppliers: { absoluteCarbonSavings: absolute(-1_717_000) @@ -1233,7 +1237,8 @@ Projects[Pages.largeWind] = new ProjectControl({ customBudgetType: { name: "Power Purchase Agreement", description: "Pay Annually", - id: 'budget' + id: 'budget', + loanTerm: 10 }, statsInfoAppliers: { absoluteCarbonSavings: absolute(-4_292_000) diff --git a/src/components/CurrentPage.tsx b/src/components/CurrentPage.tsx index 48ae1c4..46b95ac 100644 --- a/src/components/CurrentPage.tsx +++ b/src/components/CurrentPage.tsx @@ -11,7 +11,7 @@ import type { StartPageProps } from './StartPage'; import { YearRecap } from './YearRecap'; import type { PageControlProps, ControlCallbacks } from './controls'; import { CapitalFundingState } from '../Financing'; -import EndGameReport from './EndGameReport/EndGameReportPage'; +import EndGameReportPage from './EndGameReport/EndGameReportPage'; interface CurrentPageProps extends ControlCallbacks, PageControlProps { @@ -85,20 +85,19 @@ export class CurrentPage extends PureComponentIgnoreFuncs { yearRangeInitialStats={this.props.yearRangeInitialStats} handleNewYearSetup={this.props.handleNewYearSetupOnProceed} />; - case EndGameReport: + case EndGameReportPage: return ( - - - ); default: return <>; diff --git a/src/components/EndGameReport/EndGameReportPage.tsx b/src/components/EndGameReport/EndGameReportPage.tsx index 08c7a82..a7a8466 100644 --- a/src/components/EndGameReport/EndGameReportPage.tsx +++ b/src/components/EndGameReport/EndGameReportPage.tsx @@ -1,9 +1,9 @@ import React, { Fragment } from 'react'; -import { TrackedStats } from '../../trackedStats'; +import { TrackedStats, setCarbonEmissionsAndSavings, setCostPerCarbonSavings } from '../../trackedStats'; import { GameSettings } from '../SelectGameSettings'; -import { CapitalFundingState, FinancingOption, isProjectFullyFunded } from '../../Financing'; +import { CapitalFundingState, FinancingOption, getIsAnnuallyFinanced, getProjectedFinancedSpending, isProjectFullyFunded } from '../../Financing'; import { ControlCallbacks, Emphasis, PageControl } from '../controls'; -import { Box, Button, Card, CardContent, CardHeader, Grid, Link, List, ListItem, ListItemText, Tooltip, TooltipProps, Typography, styled, tooltipClasses } from '@mui/material'; +import { Box, Button, Card, CardContent, CardHeader, Grid, Link, List, ListItem, ListItemIcon, ListItemText, Tooltip, TooltipProps, Typography, styled, tooltipClasses } from '@mui/material'; import { ParentSize } from '@visx/responsive'; import { CompletedProject, ImplementedProject, ProjectControl, RenewableProject } from '../../ProjectControl'; import { ImplementedFinancingData } from '../YearRecap'; @@ -12,6 +12,7 @@ import { DialogFinancingOptionCard } from '../Dialogs/ProjectDialog'; import { parseSpecialText, truncate } from '../../functions-and-types'; import EnergyUseLineChart from '../EnergyUseLineChart'; import DownloadPDF, { ReportPDFProps } from './DownloadPDF'; +import InfoIcon from '@mui/icons-material/Info'; export default class EndGameReportPage extends React.Component { @@ -29,13 +30,15 @@ export default class EndGameReportPage extends React.Component ); @@ -69,6 +72,31 @@ function EndGameReport(props: ReportProps) { }); + let projectedFinancedSpending = getProjectedFinancedSpending(props.implementedFinancedProjects, props.renewableProjects, props.mutableStats); + let gameCurrentAndProjectedSpending = props.mutableStats.gameTotalSpending + projectedFinancedSpending; + setCarbonEmissionsAndSavings(props.mutableStats, props.defaultTrackedStats); + + setCostPerCarbonSavings(props.mutableStats, gameCurrentAndProjectedSpending); + + let endOfGameResults = { + carbonSavingsPercent: props.mutableStats.carbonSavingsPercent, + gameTotalSpending: props.mutableStats.gameTotalSpending, + projectedFinancedSpending: projectedFinancedSpending, + gameCurrentAndProjectedSpending: gameCurrentAndProjectedSpending, + costPerCarbonSavings: props.mutableStats.costPerCarbonSavings + } + const noDecimalsFormatter = Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); + const carbonSavingsPercentFormatted: string = (endOfGameResults.carbonSavingsPercent * 100).toFixed(2); + const gameTotalNetCostFormatted: string = noDecimalsFormatter.format(endOfGameResults.gameTotalSpending); + const projectedFinancedSpendingFormatted: string = noDecimalsFormatter.format(endOfGameResults.projectedFinancedSpending); + const costPerCarbonSavingsFormatted: string = endOfGameResults.costPerCarbonSavings !== undefined ? Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }).format(endOfGameResults.costPerCarbonSavings) : '0'; + return ( @@ -78,6 +106,64 @@ function EndGameReport(props: ReportProps) { parentElement={parent}/> )} + + + + + + + + + Your company has reduced CO2e Emissions by{' '} + {carbonSavingsPercentFormatted}%{' '} + + } + /> + + + + + + + You have spent{' '}${gameTotalNetCostFormatted}{' '} throughout the game. + + } + /> + + + + + + + Your cost per kg reduced was{' '}${costPerCarbonSavingsFormatted}/kg CO2e{' '} + + } + /> + + {endOfGameResults.projectedFinancedSpending > 0 && + + + + + + You are projected to spend {' '}${projectedFinancedSpendingFormatted}{' '} on financed and renewed projects in future years + + } + /> + + } + + + + { - // 1 MMBTU = 293.07107kw - let conversionFactorKwH = 293.07107; let electricityEmissions = statYear.electricityUseKWh * getElectricityEmissionsFactor(statYear.currentGameYear, statYear.gameYearInterval, statYear.gameYearDisplayOffset); let natGasEmissions = statYear.naturalGasMMBTU * statYear.naturalGasEmissionsPerMMBTU; let landfillGasEmissions = statYear.hydrogenMMBTU * statYear.hydrogenEmissionsPerMMBTU; diff --git a/src/components/YearRecap.tsx b/src/components/YearRecap.tsx index b356cd8..7db95fb 100644 --- a/src/components/YearRecap.tsx +++ b/src/components/YearRecap.tsx @@ -32,7 +32,7 @@ import { import type { ControlCallbacks, PageControl } from './controls'; import { Emphasis } from './controls'; import type { TrackedStats, YearCostSavings } from '../trackedStats'; -import { statsGaugeProperties, getYearCostSavings, setCarbonEmissionsAndSavings } from '../trackedStats'; +import { statsGaugeProperties, getYearCostSavings, setCarbonEmissionsAndSavings, setCostPerCarbonSavings } from '../trackedStats'; import type { CompletedProject, NumberApplier, RenewableProject, ProjectControl, RecapSurprise, ImplementedProject } from '../ProjectControl'; import { clampRatio, @@ -50,7 +50,7 @@ import YearRecapCharts from './YearRecapCharts'; import Projects from '../Projects'; import { ParentSize } from '@visx/responsive'; import { GameSettings } from './SelectGameSettings'; -import { CapitalFundingState, FinancingOption, getCanUseCapitalFunding, getCapitalFundingSurprise, getIsAnnuallyFinanced, isProjectFullyFunded, setCapitalFundingExpired, setCapitalFundingMilestone } from '../Financing'; +import { CapitalFundingState, FinancingOption, getCanUseCapitalFunding, getCapitalFundingSurprise, getIsAnnuallyFinanced, getProjectedFinancedSpending, isProjectFullyFunded, setCapitalFundingExpired, setCapitalFundingMilestone } from '../Financing'; import { findFinancingOptionFromProject } from '../Financing'; import { DialogFinancingOptionCard } from './Dialogs/ProjectDialog'; @@ -228,8 +228,7 @@ export class YearRecap extends React.Component { - You are projected to spend {' '}${projectedFinancedSpendingFormatted}{' '} on financed and renewed projects for the - remaining years of the game. You are projected to spend {' '}${gameCurrentAndProjectedSpendingFormatted}{' '} total over the course of the game. + You are projected to spend {' '}${projectedFinancedSpendingFormatted}{' '} on financed and renewed projects. Your total projected spend is {' '}${gameCurrentAndProjectedSpendingFormatted}{' '}. } /> @@ -443,25 +442,15 @@ export class YearRecap extends React.Component { this.addCapitalFundingRewardCard(recapResults.projectRecapCards, mutableCapitalFundingState, mutableStats); mutableStats.yearEndTotalSpending = recapResults.yearEndTotalSpending; mutableStats.gameTotalSpending = initialCurrentYearStats.gameTotalSpending + recapResults.yearEndTotalSpending; - recapResults.projectedFinancedSpending = this.getProjectedFinancedSpending(implementedFinancedProjects, implementedRenewableProjectsCopy, mutableStats); + recapResults.projectedFinancedSpending = getProjectedFinancedSpending(implementedFinancedProjects, implementedRenewableProjectsCopy, mutableStats); recapResults.gameCurrentAndProjectedSpending = mutableStats.gameTotalSpending + recapResults.projectedFinancedSpending; - this.setCostPerCarbonSavings(mutableStats, recapResults.gameCurrentAndProjectedSpending); + setCostPerCarbonSavings(mutableStats, recapResults.gameCurrentAndProjectedSpending); recapResults.yearCostSavings = getYearCostSavings(initialCurrentYearStats, mutableStats); this.setRenewableProjectResults(implementedRenewableProjectsCopy, mutableStats, initialCurrentYearStats, recapResults.yearCostSavings); return recapResults; } - /** - * Set mutable stats costPerCarbonSavings - */ - setCostPerCarbonSavings(mutableStats: TrackedStats, gameCurrentAndProjectedSpending: number) { - let costPerCarbonSavings = 0; - if (gameCurrentAndProjectedSpending > 0 && mutableStats.carbonSavingsPerKg > 0) { - costPerCarbonSavings = gameCurrentAndProjectedSpending / mutableStats.carbonSavingsPerKg; - } - mutableStats.costPerCarbonSavings = costPerCarbonSavings; - } /** * Costs from completed projects still in financing @@ -481,32 +470,6 @@ export class YearRecap extends React.Component { return yearFinancingCosts; } - /** - * Future costs from financed projects and renewed PPAs still being paid on - */ - getProjectedFinancedSpending(implementedFinancedProjects: ImplementedProject[], renewableProjects: ImplementedProject[], mutableStats: TrackedStats) { - let futureFinancedSpending: number = implementedFinancedProjects.reduce((totalFutureSpending: number, project: ImplementedProject) => { - return this.getRemainingProjectCosts(project, mutableStats, totalFutureSpending); - }, 0); - let PPAProjects = renewableProjects.filter(project => Projects[project.page].isPPA); - futureFinancedSpending += PPAProjects.reduce((totalFutureSpending: number, project: ImplementedProject) => { - return this.getRemainingProjectCosts(project, mutableStats, totalFutureSpending); - }, 0); - return futureFinancedSpending; - } - - getRemainingProjectCosts(project: ImplementedProject, mutableStats: TrackedStats, totalFutureSpending: number) { - let yearsRemaining = 10 - mutableStats.currentGameYear; - let projectControl = Projects[project.page]; - let isAnnuallyFinanced = getIsAnnuallyFinanced(project.financingOption.financingType.id); - if ((isAnnuallyFinanced || projectControl.isPPA) && yearsRemaining) { - let annualCost = projectControl.getImplementationCost(project.financingOption.financingType.id, mutableStats.gameYearInterval); - let futureCosts = annualCost * yearsRemaining; - totalFutureSpending += futureCosts; - } - return totalFutureSpending; - } - /** * WARNING - Directly mutates renewable project in first year. This is a workaround to get correct stats display and state given some of the other game mechanics and logic * we need to assign/save individualized project savings to be applied in each renewable year recap - later years don't change savings state, only display values diff --git a/src/trackedStats.tsx b/src/trackedStats.tsx index b582de0..87d3366 100644 --- a/src/trackedStats.tsx +++ b/src/trackedStats.tsx @@ -182,6 +182,17 @@ export function setCarbonEmissionsAndSavings(newStats: TrackedStats, defaultTrac return newStats; } +/** +* Set mutable stats costPerCarbonSavings +*/ +export function setCostPerCarbonSavings(mutableStats: TrackedStats, gameCurrentAndProjectedSpending: number) { + let costPerCarbonSavings = 0; + if (gameCurrentAndProjectedSpending > 0 && mutableStats.carbonSavingsPerKg > 0) { + costPerCarbonSavings = gameCurrentAndProjectedSpending / mutableStats.carbonSavingsPerKg; + } + mutableStats.costPerCarbonSavings = costPerCarbonSavings; +} + export function getYearCostSavings(oldStats: TrackedStats, newStats: TrackedStats): YearCostSavings { let oldNgCost = oldStats.naturalGasCostPerMMBTU * oldStats.naturalGasMMBTU; let newNgCost = newStats.naturalGasCostPerMMBTU * newStats.naturalGasMMBTU;