Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
faf7d7d
Merge pull request #1 from YourDeveloperFriend/main
lordscarlet Oct 16, 2025
ee5a76d
Merge pull request #2 from YourDeveloperFriend/main
lordscarlet Oct 24, 2025
680fa3c
Merge remote-tracking branch 'upstream/main'
lordscarlet Feb 20, 2026
b0b8c22
Merge remote-tracking branch 'upstream/main'
lordscarlet Mar 2, 2026
f023804
Add map-specific breakdown details and multiplier fixes
lordscarlet Mar 3, 2026
309cc2c
Add unit tests for multipliers and breakdown math
lordscarlet Mar 3, 2026
a1f3a4a
Add smoke e2e coverage for player overview breakdowns
lordscarlet Mar 3, 2026
ae40daa
Display monsoon probabilities as percentages with odds (e.g., 67% (2 …
lordscarlet Mar 4, 2026
a20c852
Add e2e artifacts to gitignore
lordscarlet Mar 4, 2026
9eca900
Fix gitignore formatting - separate *.tsbuildinfo and src/e2e/artifacts/
lordscarlet Mar 4, 2026
cba25fd
Address PR review: use multiplier hooks in base score calculations
lordscarlet Mar 4, 2026
cccded9
Address PR threads for map breakdown follow-ups
lordscarlet Mar 4, 2026
3e78d85
Fix test compilation by isolating monsoon scenarios from client depen…
lordscarlet Mar 4, 2026
9319c09
Merge branch 'main' into feature/map-specific-breakdown-details
lordscarlet May 2, 2026
f204e16
Merge upstream/main into feature/map-specific-breakdown-details
lordscarlet May 3, 2026
6159fcb
Merge origin/feature/map-specific-breakdown-details into feature/map-…
lordscarlet May 3, 2026
91a5330
Fix PR 230 CI failures: format files and remove unused export
lordscarlet May 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ builds
buildmeta.*
*.swp
*.tsbuildinfo
src/e2e/artifacts/
scripts/seed_map_ratings_from_csv.js
75 changes: 75 additions & 0 deletions src/client/game/player_expanded_row.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@
color: #d1d5db;
}

.panelSubsectionTitle {
font-weight: 600;
font-size: 13px;
color: #6b7280;
margin-top: 8px;
margin-bottom: 6px;
}

:global(.dark-mode) .panelSubsectionTitle {
color: #9ca3af;
}

.panelCard {
background: white;
padding: 12px 16px;
Expand Down Expand Up @@ -371,6 +383,69 @@
margin-top: 4px;
}

/* Monsoon scenarios */
.monsoonScenariosGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-top: 8px;
}

.monsoonScenario {
text-align: center;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 6px;
}

:global(.dark-mode) .monsoonScenario {
border-color: #555;
}

.monsoonLabel {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
font-weight: 500;
}

:global(.dark-mode) .monsoonLabel {
color: #9ca3af;
}

.monsoonValue {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
}

.monsoonCost {
font-size: 11px;
color: #6b7280;
margin-bottom: 2px;
}

:global(.dark-mode) .monsoonCost {
color: #9ca3af;
}

.monsoonProbability {
font-size: 11px;
color: #6b7280;
font-style: italic;
}

:global(.dark-mode) .monsoonProbability {
color: #9ca3af;
}

.monsoonNeeds {
font-size: 11px;
color: #dc2626;
margin-top: 4px;
font-weight: 500;
}

/* Eliminated player expanded */
.eliminatedMessage {
padding: 48px 24px;
Expand Down
109 changes: 104 additions & 5 deletions src/client/game/player_expanded_row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
useInjected,
useInjectedState,
usePhaseState,
useViewSettings,
} from "../utils/injection_context";
import { Icon } from "semantic-ui-react";

Expand Down Expand Up @@ -165,14 +166,17 @@ function WarningBanners({ player }: { player: PlayerData }) {

function FinancialDetailsPanel({ player }: { player: PlayerData }) {
const profitHelper = useInjected(ProfitHelper);
const viewSettings = useViewSettings();
const income = profitHelper.getIncome(player);
const expenses = profitHelper.getExpenses(player);
const profit = profitHelper.getProfit(player);
const endOfTurnMoney = player.money + profit;
const warning = getPlayerWarning(player);

const netIncomeHighlight =
warning.hasIncomeLoss || warning.hasEliminationRisk;
const customExpenseItems =
viewSettings.useExpenseBreakdownItems?.(player) ?? [];
const monsoonScenarios = viewSettings.useMonsoonScenarios?.(player) ?? [];

return (
<div className={styles.panelSection}>
Expand All @@ -194,6 +198,7 @@ function FinancialDetailsPanel({ player }: { player: PlayerData }) {
-{formatMoney(expenses)}
</span>
</div>
{/* TODO: Maps with zero expenses (Denmark) might benefit from an explanatory note instead of showing $0 items. Consider adding useExpenseBreakdownNote hook in future. */}
<div className={styles.panelSubrow}>
<span>• Locomotive maintenance:</span>
<span>-{formatMoney(player.locomotive)}</span>
Expand All @@ -202,6 +207,12 @@ function FinancialDetailsPanel({ player }: { player: PlayerData }) {
<span>• Share interest:</span>
<span>-{formatMoney(player.shares)}</span>
</div>
{customExpenseItems.map((item, index) => (
<div key={index} className={styles.panelSubrow}>
<span>• {item.label}</span>
<span>-{formatMoney(item.value)}</span>
</div>
))}
<div
className={`${styles.panelRow} ${styles.panelDivider} ${netIncomeHighlight ? styles.panelHighlight : ""}`}
>
Expand All @@ -222,6 +233,44 @@ function FinancialDetailsPanel({ player }: { player: PlayerData }) {
: `${formatMoney(endOfTurnMoney)} (needs ${formatMoney(Math.abs(endOfTurnMoney))})`}
</span>
</div>
{monsoonScenarios.length > 0 && (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to move this map-specific "monsoon scenarios" behavior into the map and make this a more generic extension point (similar to the custom expense items)? My hope is that that would also let us move the new monsoon-specific CSS into a map-specific CSS module as well.

<>
<div className={styles.panelDivider} />
<div className={styles.panelSubsectionTitle}>
Monsoon Scenarios (next income phase):
</div>
<div className={styles.monsoonScenariosGrid}>
{monsoonScenarios.map((scenario, index) => {
const resultingMoney = endOfTurnMoney - scenario.cost;
return (
<div key={index} className={styles.monsoonScenario}>
<div className={styles.monsoonLabel}>
{scenario.description}
</div>
<div
className={`${styles.monsoonValue} ${resultingMoney >= 0 ? styles.valuePositive : styles.valueNegative}`}
>
{formatMoney(resultingMoney)}
</div>
<div className={styles.monsoonCost}>
{scenario.cost === 0
? "No cost"
: `Cost: ${formatMoney(scenario.cost)}`}
</div>
<div className={styles.monsoonProbability}>
{scenario.probability}
</div>
{resultingMoney < 0 && (
<div className={styles.monsoonNeeds}>
Needs {formatMoney(Math.abs(resultingMoney))}
</div>
)}
</div>
);
})}
</div>
</>
)}
</div>
</div>
);
Expand Down Expand Up @@ -319,27 +368,39 @@ function LocoUpgradeImpactPanel({ player }: { player: PlayerData }) {

function ScoreBreakdownPanel({ player }: { player: PlayerData }) {
const playerHelper = useInjected(PlayerHelper);
const viewSettings = useViewSettings();
const incomePoints = playerHelper.getScoreFromIncome(player);
const sharePoints = playerHelper.getScoreFromShares(player);
const trackPoints = playerHelper.getScoreFromTrack(player);
const trackCount = playerHelper.countTrack(player.color);
const totalScore = playerHelper.getScore(player)[0];

// Get multipliers from helper (handles maps with custom multipliers)
const incomeMultiplier = playerHelper.getIncomeMultiplier();
const sharesMultiplier = playerHelper.getSharesMultiplier();

const customScoreItems = viewSettings.useScoreBreakdownItems?.(player) ?? [];

// Hide score breakdown for maps with non-VP scoring (e.g., Detroit, Barbados)
if (viewSettings.hideScoreBreakdown) {
return null;
}

return (
<div className={styles.panelSection}>
<div className={styles.panelTitle}>Score Breakdown</div>
<div className={styles.panelCard}>
<div className={styles.panelRow}>
<span className={styles.panelLabel}>
Income points ({player.income} × 3):
Income points ({player.income} × {incomeMultiplier}):
</span>
<span className={`${styles.panelValue} ${styles.valuePositive}`}>
+{incomePoints}
</span>
</div>
<div className={styles.panelRow}>
<span className={styles.panelLabel}>
Share penalty ({player.shares} × -3):
Share penalty ({player.shares} × {sharesMultiplier}):
</span>
<span className={`${styles.panelValue} ${styles.valueNegative}`}>
{sharePoints}
Expand All @@ -353,6 +414,20 @@ function ScoreBreakdownPanel({ player }: { player: PlayerData }) {
+{trackPoints}
</span>
</div>
{/* TODO: Maps with complex formulas (Puerto Rico) could add breakdown items explaining adjustments (e.g., black cube income penalty). */}
{customScoreItems.map((item, index) => (
<div key={index} className={styles.panelRow}>
<span className={styles.panelLabel}>{item.label}</span>
<span
className={`${styles.panelValue} ${
item.value >= 0 ? styles.valuePositive : styles.valueNegative
}`}
>
{item.value >= 0 ? "+" : ""}
{item.value}
</span>
</div>
))}
<div className={`${styles.panelRow} ${styles.panelDivider}`}>
<span className={`${styles.panelLabel} ${styles.panelBold}`}>
Total Score:
Expand Down Expand Up @@ -473,28 +548,39 @@ interface ScoreTooltipContentProps {

export function ScoreTooltipContent({ player }: ScoreTooltipContentProps) {
const playerHelper = useInjected(PlayerHelper);
const viewSettings = useViewSettings();
const incomePoints = playerHelper.getScoreFromIncome(player);
const sharePoints = playerHelper.getScoreFromShares(player);
const trackPoints = playerHelper.getScoreFromTrack(player);
const trackCount = playerHelper.countTrack(player.color);
const totalScore = playerHelper.getScore(player)[0];

// Get multipliers from helper (handles maps with custom multipliers)
const incomeMultiplier = playerHelper.getIncomeMultiplier();
const sharesMultiplier = playerHelper.getSharesMultiplier();

if (viewSettings.hideScoreBreakdown) {
return null;
}

const customScoreItems = viewSettings.useScoreBreakdownItems?.(player) ?? [];

Comment thread
lordscarlet marked this conversation as resolved.
return (
<div className={styles.tooltipContent}>
<div className={styles.tooltipTitle}>Score Breakdown</div>
<table className={styles.tooltipTable}>
<tbody>
<tr>
<td className={styles.tooltipLabel}>
Income points ({player.income} × 3):
Income points ({player.income} × {incomeMultiplier}):
</td>
<td className={`${styles.tooltipValue} ${styles.valuePositive}`}>
+{incomePoints}
</td>
</tr>
<tr>
<td className={styles.tooltipLabel}>
Share penalty ({player.shares} × -3):
Share penalty ({player.shares} × {sharesMultiplier}):
</td>
<td className={`${styles.tooltipValue} ${styles.valueNegative}`}>
{sharePoints}
Expand All @@ -508,6 +594,19 @@ export function ScoreTooltipContent({ player }: ScoreTooltipContentProps) {
+{trackPoints}
</td>
</tr>
{customScoreItems.map((item, index) => (
<tr key={index}>
<td className={styles.tooltipLabel}>{item.label}</td>
<td
className={`${styles.tooltipValue} ${
item.value >= 0 ? styles.valuePositive : styles.valueNegative
}`}
>
{item.value >= 0 ? "+" : ""}
{item.value}
</td>
</tr>
))}
<tr className={styles.tooltipTotalRow}>
<td className={styles.tooltipLabel}>Total:</td>
<td className={styles.tooltipValue}>{totalScore}</td>
Expand Down
3 changes: 3 additions & 0 deletions src/e2e/e2e_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { buildingTrack } from "./build_track_test";
import { creatingGame } from "./create_game_test";
import { playerOverviewBreakdowns } from "./player_overview_breakdowns_test";
import { setUpServer } from "./util/server";
import { setTestTimeout } from "./util/timeout";
import { Driver, setUpWebDriver } from "./util/webdriver";
Expand All @@ -19,4 +20,6 @@ describe("e2e tests", () => {

describe("Building track", () => buildingTrack(driver));
describe("creating game", () => creatingGame(driver));
describe("player overview breakdowns", () =>
playerOverviewBreakdowns(driver));
});
Loading
Loading