diff --git a/src-tauri/crates/olm_core/src/end_of_season.rs b/src-tauri/crates/olm_core/src/end_of_season.rs index d0a81de0..53d77433 100644 --- a/src-tauri/crates/olm_core/src/end_of_season.rs +++ b/src-tauri/crates/olm_core/src/end_of_season.rs @@ -102,11 +102,18 @@ fn prize_money_for_position(position: u32) -> i64 { .unwrap_or(150_000) } -fn refresh_hiring_cycle_budgets(team: &mut crate::domain::team::Team) { - // Minimal hook: after split settlements (prize/objectives), rebalance next-cycle - // planning budgets from current treasury so offseason hiring decisions have - // coherent funds available without a full finance redesign. - team.wage_budget = ((team.finance.max(0) as f64) * 0.06).round() as i64; +fn refresh_hiring_cycle_budgets( + team: &mut crate::domain::team::Team, + annual_wage_bill: i64, +) { + // Rebalance next-cycle planning budgets from current treasury so offseason + // hiring decisions have coherent funds available without a full finance redesign. + // + // CRITICAL: the new wage budget must never be lower than the current annual + // wage bill. Otherwise a team that made no signings would instantly show + // an absurd usage percentage (e.g. 492 %) after the split transition. + let computed = ((team.finance.max(0) as f64) * 0.06).round() as i64; + team.wage_budget = computed.max(annual_wage_bill); team.transfer_budget = ((team.finance.max(0) as f64) * 0.22).round() as i64; } @@ -358,7 +365,18 @@ fn process_end_of_season_inner( total_teams: final_standings.len() as u32, }; - // 4. Record team season history + // 4. Pre-calculate annual wage bills for the budget refresh below + // (must be done before the mutable team loop to avoid borrow conflicts). + let wage_bills: std::collections::HashMap = final_standings + .iter() + .filter_map(|standing| { + let team_id = &standing.team_id; + let wages = crate::finances::calc_annual_wages(game, team_id); + Some((team_id.clone(), wages)) + }) + .collect(); + + // 5. Record team season history for (idx, standing) in final_standings.iter().enumerate() { if let Some(team) = game.teams.iter_mut().find(|t| t.id == standing.team_id) { let position = (idx + 1) as u32; @@ -399,7 +417,8 @@ fn process_end_of_season_inner( .ok(); } - refresh_hiring_cycle_budgets(team); + let annual_wage = wage_bills.get(&team.id).copied().unwrap_or(0); + refresh_hiring_cycle_budgets(team, annual_wage); } } diff --git a/src/ui-v2/AppV2.tsx b/src/ui-v2/AppV2.tsx index 9bf25e27..db4aeb78 100644 --- a/src/ui-v2/AppV2.tsx +++ b/src/ui-v2/AppV2.tsx @@ -102,6 +102,7 @@ export default function AppV2() { } = useUpdater(AUTO_CHECK_UPDATES); const [ready, setReady] = useState(false); + const [safeAreaTop, setSafeAreaTop] = useState(0); useEffect(() => { if (!loaded) loadSettings(); @@ -121,6 +122,7 @@ export default function AppV2() { } }, [ready, settings.language]); + // --- Android: immersive mode + safe-area --- useEffect(() => { if (!ready) return; const isAndroid = /Android/i.test(window.navigator.userAgent); @@ -148,6 +150,23 @@ export default function AppV2() { void applyAndroidImmersive(); + // Measure the actual safe-area inset by reading env(safe-area-inset-top). + // Chromium WebViews on Android may not expose env(), so fall back to 48 px + // (typical Android status bar height in landscape). + const measureSafeArea = () => { + if (cancelled) return; + const probe = document.createElement("div"); + probe.style.cssText = + "position:fixed;top:env(safe-area-inset-top,48px);left:0;width:1px;height:1px;pointer-events:none;opacity:0;z-index:-1"; + document.body.appendChild(probe); + // getComputedStyle resolves env() to its computed pixel value, or 48px + const raw = parseFloat(getComputedStyle(probe).top); + setSafeAreaTop(Number.isFinite(raw) && raw > 0 ? Math.round(raw) : 48); + document.body.removeChild(probe); + }; + // Wait for the WebView to settle before measuring. + setTimeout(measureSafeArea, 400); + const onVisible = () => { if (document.visibilityState === "visible") { void applyAndroidImmersive(); @@ -220,7 +239,10 @@ export default function AppV2() { }, []); return ( -
+
0 ? { paddingTop: safeAreaTop } : undefined} + >