diff --git a/apps/web/src/components/game/CompanyStatsCard.tsx b/apps/web/src/components/game/CompanyStatsCard.tsx new file mode 100644 index 0000000..c7333cf --- /dev/null +++ b/apps/web/src/components/game/CompanyStatsCard.tsx @@ -0,0 +1,65 @@ +import { useGameStore } from '@/store'; +import { formatMoney, formatNumber, formatPercent } from '@ai-tycoon/shared'; +import { Share2, Copy, Check } from 'lucide-react'; +import { useState } from 'react'; +import { ACHIEVEMENT_DEFINITIONS } from '@ai-tycoon/game-engine'; + +export function CompanyStatsCard({ onClose }: { onClose: () => void }) { + const [copied, setCopied] = useState(false); + const companyName = useGameStore((s) => s.meta.companyName); + const era = useGameStore((s) => s.meta.currentEra); + const totalPlayTime = useGameStore((s) => s.meta.totalPlayTime); + const money = useGameStore((s) => s.economy.money); + const totalRevenue = useGameStore((s) => s.economy.totalRevenue); + const valuation = useGameStore((s) => s.economy.funding.valuation); + const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers); + const models = useGameStore((s) => s.models.trainedModels.length); + const bestModel = useGameStore((s) => + s.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0), + ); + const reputation = useGameStore((s) => s.reputation.score); + const achievements = useGameStore((s) => s.achievements.unlocked.length); + const dataCenters = useGameStore((s) => s.infrastructure.dataCenters.length); + + const eraLabel = era === 'startup' ? 'Startup' : era === 'scaleup' ? 'Scale-up' : era === 'bigtech' ? 'Big Tech' : 'AGI'; + const hours = Math.floor(totalPlayTime / 3600); + const minutes = Math.floor((totalPlayTime % 3600) / 60); + + const statsText = [ + `${companyName} — AI Tycoon`, + `Era: ${eraLabel} | Playtime: ${hours}h ${minutes}m`, + `Cash: ${formatMoney(money)} | Revenue: ${formatMoney(totalRevenue)}`, + `Valuation: ${formatMoney(valuation)}`, + `Subscribers: ${formatNumber(subscribers)} | Models: ${models}`, + `Best Model: ${bestModel.toFixed(1)}/100 | Reputation: ${reputation}/100`, + `Data Centers: ${dataCenters} | Achievements: ${achievements}/${ACHIEVEMENT_DEFINITIONS.length}`, + ].join('\n'); + + const handleCopy = () => { + navigator.clipboard.writeText(statsText); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
e.stopPropagation()}> +
+

Company Stats

+ +
+ +
+ {statsText} +
+ + +
+
+ ); +} diff --git a/apps/web/src/components/layout/MainLayout.tsx b/apps/web/src/components/layout/MainLayout.tsx index c4eaa25..7b36633 100644 --- a/apps/web/src/components/layout/MainLayout.tsx +++ b/apps/web/src/components/layout/MainLayout.tsx @@ -14,6 +14,7 @@ import { TalentPage } from '@/pages/TalentPage'; import { DataPage } from '@/pages/DataPage'; import { CompetitorsPage } from '@/pages/CompetitorsPage'; import { AchievementsPage } from '@/pages/AchievementsPage'; +import { LeaderboardPage } from '@/pages/LeaderboardPage'; export function MainLayout() { const activePage = useGameStore((s) => s.activePage); @@ -45,6 +46,7 @@ function PageRouter({ page }: { page: string }) { case 'data': return ; case 'competitors': return ; case 'achievements': return ; + case 'leaderboard': return ; case 'settings': return ; default: return ; } diff --git a/apps/web/src/components/layout/Sidebar.tsx b/apps/web/src/components/layout/Sidebar.tsx index ec06cb0..3550c4e 100644 --- a/apps/web/src/components/layout/Sidebar.tsx +++ b/apps/web/src/components/layout/Sidebar.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import { LayoutDashboard, Server, FlaskConical, Brain, - TrendingUp, Users, Database, Swords, DollarSign, Settings, Trophy, + TrendingUp, Users, Database, Swords, DollarSign, Settings, Trophy, Medal, } from 'lucide-react'; import { useGameStore, type ActivePage } from '@/store'; @@ -16,6 +16,7 @@ const NAV_ITEMS: { page: ActivePage; label: string; icon: typeof LayoutDashboard { page: 'data', label: 'Data', icon: Database, era: 'scaleup' }, { page: 'competitors', label: 'Competitors', icon: Swords, era: 'scaleup' }, { page: 'achievements', label: 'Achievements', icon: Trophy }, + { page: 'leaderboard', label: 'Leaderboard', icon: Medal }, { page: 'settings', label: 'Settings', icon: Settings }, ]; diff --git a/apps/web/src/components/layout/TopBar.tsx b/apps/web/src/components/layout/TopBar.tsx index 3cc92c0..78f48e4 100644 --- a/apps/web/src/components/layout/TopBar.tsx +++ b/apps/web/src/components/layout/TopBar.tsx @@ -1,5 +1,6 @@ -import { type ReactNode } from 'react'; -import { Pause, Play, Bell } from 'lucide-react'; +import { type ReactNode, useState } from 'react'; +import { Pause, Play, Bell, Share2 } from 'lucide-react'; +import { CompanyStatsCard } from '@/components/game/CompanyStatsCard'; import { useGameStore } from '@/store'; import { formatMoney, formatNumber, formatDuration, formatPercent } from '@ai-tycoon/shared'; import type { GameSpeed } from '@ai-tycoon/shared'; @@ -21,6 +22,7 @@ export function TopBar() { const setGameSpeed = useGameStore((s) => s.setGameSpeed); const notifications = useGameStore((s) => s.notifications); const unreadCount = notifications.filter(n => !n.read).length; + const [showStats, setShowStats] = useState(false); const netIncome = revenuePerTick - expensesPerTick; @@ -86,6 +88,14 @@ export function TopBar() { ))} + + + + {showStats && setShowStats(false)} />} ); } diff --git a/apps/web/src/pages/LeaderboardPage.tsx b/apps/web/src/pages/LeaderboardPage.tsx new file mode 100644 index 0000000..aa6f353 --- /dev/null +++ b/apps/web/src/pages/LeaderboardPage.tsx @@ -0,0 +1,124 @@ +import { useState, useEffect } from 'react'; +import { Trophy, Medal, Clock, TrendingUp } from 'lucide-react'; +import { useGameStore } from '@/store'; +import { formatMoney, formatNumber } from '@ai-tycoon/shared'; +import { api, getAuthToken } from '@/lib/api'; + +interface LeaderboardEntry { + companyName: string; + score: number; + era: string; + tickCount: number; +} + +const CATEGORIES = [ + { id: 'revenue', label: 'Highest Revenue', icon: TrendingUp }, + { id: 'fastest-agi', label: 'Fastest to AGI', icon: Clock }, + { id: 'capability', label: 'Best Model', icon: Trophy }, +] as const; + +export function LeaderboardPage() { + const [category, setCategory] = useState('revenue'); + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(false); + const [submitted, setSubmitted] = useState(false); + const companyName = useGameStore((s) => s.meta.companyName); + const totalRevenue = useGameStore((s) => s.economy.totalRevenue); + const era = useGameStore((s) => s.meta.currentEra); + const tickCount = useGameStore((s) => s.meta.tickCount); + const bestModel = useGameStore((s) => + s.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0), + ); + + useEffect(() => { + setLoading(true); + api.leaderboard.get(category) + .then(data => setEntries(data.entries)) + .catch(() => setEntries([])) + .finally(() => setLoading(false)); + }, [category]); + + const handleSubmit = async () => { + if (!getAuthToken()) return; + const scoreMap: Record = { + revenue: Math.floor(totalRevenue), + 'fastest-agi': era === 'agi' ? tickCount : 0, + capability: Math.floor(bestModel * 10), + }; + const score = scoreMap[category]; + if (score <= 0) return; + try { + await api.leaderboard.submit({ companyName, category, score, era, tickCount }); + setSubmitted(true); + const data = await api.leaderboard.get(category); + setEntries(data.entries); + } catch { /* ignore */ } + }; + + return ( +
+

Leaderboard

+ +
+ {CATEGORIES.map(cat => ( + + ))} +
+ + {getAuthToken() && !submitted && ( + + )} + {submitted &&

Score submitted!

} + +
+ {loading ? ( +
Loading...
+ ) : entries.length === 0 ? ( +
+ +

No entries yet. Be the first!

+
+ ) : ( + + + + + + + + + + + {entries.map((entry, i) => ( + + + + + + + ))} + +
#CompanyScoreEra
+ {i < 3 ? : i + 1} + {entry.companyName}{formatNumber(entry.score)}{entry.era}
+ )} +
+
+ ); +} diff --git a/apps/web/src/store/index.ts b/apps/web/src/store/index.ts index 8d590db..8b417d7 100644 --- a/apps/web/src/store/index.ts +++ b/apps/web/src/store/index.ts @@ -23,7 +23,7 @@ import { import { INITIAL_RIVALS } from '@ai-tycoon/game-engine'; export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models' - | 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'settings'; + | 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'leaderboard' | 'settings'; interface UIState { activePage: ActivePage; @@ -252,18 +252,28 @@ export const useGameStore = create()( let money = s.economy.money; let reputation = { ...s.reputation }; + let talent = { ...s.talent }; const consequences = choice.consequences; for (const c of consequences) { switch (c.type) { case 'money': money += c.value; break; case 'reputation': reputation = { ...reputation, score: Math.min(100, Math.max(0, reputation.score + c.value)), publicPerception: Math.min(100, Math.max(0, reputation.publicPerception + c.value)) }; break; + case 'regulation': reputation = { ...reputation, regulatoryStanding: Math.min(100, Math.max(0, reputation.regulatoryStanding + c.value)) }; break; + case 'talent': { + const dept = c.target as keyof typeof talent.departments | undefined; + if (dept && talent.departments[dept]) { + talent = { ...talent, departments: { ...talent.departments, [dept]: { ...talent.departments[dept], headcount: Math.max(0, talent.departments[dept].headcount + c.value) } } }; + } + break; + } } } return { economy: { ...s.economy, money: Math.max(0, money) }, reputation, + talent, events: { ...s.events, activeEvents: s.events.activeEvents.filter(e => e.instanceId !== instanceId), diff --git a/packages/game-engine/src/data/events.ts b/packages/game-engine/src/data/events.ts index ba5f269..cc932e5 100644 --- a/packages/game-engine/src/data/events.ts +++ b/packages/game-engine/src/data/events.ts @@ -1559,4 +1559,237 @@ export const EVENT_DEFINITIONS: EventDefinition[] = [ defaultChoiceIndex: 0, expiryTicks: 360, }, + + // ============================================================ + // GEOPOLITICAL EVENTS (6) + // ============================================================ + { + id: 'geo_export_controls', + title: 'New AI Export Controls', + descriptionTemplate: + 'The government has announced export controls on advanced AI chips and models. International operations may be affected.', + category: 'regulatory', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 6, + cooldownTicks: 1200, + maxOccurrences: 2, + prerequisites: [], + conditions: [{ field: 'models.trainedModels.length', operator: 'gte', value: 2 }], + choices: [ + { + label: 'Lobby for exemptions', + description: 'Spend money to lobby policymakers for carve-outs.', + consequences: [ + { type: 'money', value: -100000 }, + { type: 'regulation', value: 10 }, + ], + }, + { + label: 'Comply and adapt', + description: 'Accept the restrictions and restructure international operations.', + consequences: [ + { type: 'reputation', value: 5 }, + { type: 'regulation', value: 5 }, + { type: 'money', value: -30000 }, + ], + }, + { + label: 'Challenge in court', + description: 'File a legal challenge. Risky but could set favorable precedent.', + consequences: [ + { type: 'money', value: -200000 }, + { type: 'regulation', value: -10 }, + { type: 'reputation', value: -5 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 300, + }, + { + id: 'geo_energy_crisis', + title: 'Energy Crisis in Data Center Region', + descriptionTemplate: + 'A regional energy crisis is driving electricity costs up 40% in key data center locations. Your infrastructure costs are spiking.', + category: 'market', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 5, + cooldownTicks: 1500, + maxOccurrences: 2, + prerequisites: [], + conditions: [{ field: 'infrastructure.dataCenters.length', operator: 'gte', value: 1 }], + choices: [ + { + label: 'Negotiate long-term energy contracts', + description: 'Lock in rates now before they go higher. Requires upfront capital.', + consequences: [ + { type: 'money', value: -150000 }, + ], + }, + { + label: 'Invest in on-site renewable energy', + description: 'Install solar panels and battery storage. High upfront cost, long-term savings.', + consequences: [ + { type: 'money', value: -300000 }, + { type: 'reputation', value: 10 }, + ], + }, + { + label: 'Absorb the costs', + description: 'Take the hit and hope prices normalize.', + consequences: [ + { type: 'money', value: -50000 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 300, + }, + { + id: 'geo_natural_disaster', + title: 'Natural Disaster Threatens Data Center', + descriptionTemplate: + 'Severe weather has been reported near one of your data center locations. Your ops team is preparing contingency plans.', + category: 'industry', + eras: ['startup', 'scaleup', 'bigtech', 'agi'], + weight: 4, + cooldownTicks: 1800, + maxOccurrences: 3, + prerequisites: [], + conditions: [{ field: 'infrastructure.dataCenters.length', operator: 'gte', value: 1 }], + choices: [ + { + label: 'Activate disaster recovery', + description: 'Failover to backup systems. Costs money but protects uptime.', + consequences: [ + { type: 'money', value: -50000 }, + { type: 'reputation', value: 3 }, + ], + }, + { + label: 'Ride it out', + description: 'Hope the storm misses. Save money but risk downtime.', + consequences: [ + { type: 'reputation', value: -8 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 180, + }, + { + id: 'geo_ai_safety_summit', + title: 'International AI Safety Summit', + descriptionTemplate: + 'World leaders are convening a summit on AI safety. You have been invited to present your company\'s approach to responsible AI.', + category: 'regulatory', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 6, + cooldownTicks: 1500, + maxOccurrences: 2, + prerequisites: [], + conditions: [{ field: 'reputation.score', operator: 'gte', value: 30 }], + choices: [ + { + label: 'Sign voluntary safety commitments', + description: 'Join the safety pledge. Good PR, but constrains future development speed.', + consequences: [ + { type: 'reputation', value: 15 }, + { type: 'regulation', value: 10 }, + { type: 'research_speed', value: -0.05 }, + ], + }, + { + label: 'Attend but make no commitments', + description: 'Show up, listen, and keep your options open.', + consequences: [ + { type: 'reputation', value: 3 }, + ], + }, + { + label: 'Skip the summit', + description: 'Your engineers have models to train. No time for politics.', + consequences: [ + { type: 'reputation', value: -10 }, + { type: 'regulation', value: -5 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 240, + }, + { + id: 'geo_talent_immigration', + title: 'Immigration Policy Changes', + descriptionTemplate: + 'New immigration restrictions are making it harder to recruit international AI talent. Your HR team is concerned about pipeline impact.', + category: 'regulatory', + eras: ['scaleup', 'bigtech'], + weight: 4, + cooldownTicks: 1200, + maxOccurrences: 1, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Open a remote research lab abroad', + description: 'Set up a satellite research office in a talent-friendly country.', + consequences: [ + { type: 'money', value: -200000 }, + { type: 'talent', value: 5, target: 'research' }, + ], + }, + { + label: 'Increase domestic salaries', + description: 'Compete harder for local talent with premium compensation.', + consequences: [ + { type: 'money', value: -100000 }, + { type: 'talent', value: 2, target: 'research' }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 360, + }, + { + id: 'geo_data_sovereignty', + title: 'Data Sovereignty Regulations', + descriptionTemplate: + 'Multiple countries are enacting data localization laws. User data must be stored within national borders, complicating your global infrastructure.', + category: 'regulatory', + eras: ['bigtech', 'agi'], + weight: 5, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [{ field: 'market.consumers.totalSubscribers', operator: 'gte', value: 5000 }], + choices: [ + { + label: 'Build regional data centers', + description: 'Comply by deploying infrastructure in affected regions. Expensive but thorough.', + consequences: [ + { type: 'money', value: -500000 }, + { type: 'regulation', value: 15 }, + { type: 'reputation', value: 5 }, + ], + }, + { + label: 'Withdraw from those markets', + description: 'Stop serving users in affected regions to avoid compliance costs.', + consequences: [ + { type: 'reputation', value: -10 }, + ], + }, + { + label: 'Implement data partitioning', + description: 'Use technical solutions to segment data by region without new DCs.', + consequences: [ + { type: 'money', value: -100000 }, + { type: 'regulation', value: 5 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 360, + }, ]; diff --git a/packages/game-engine/src/systems/economySystem.ts b/packages/game-engine/src/systems/economySystem.ts index e024ec4..4ebab07 100644 --- a/packages/game-engine/src/systems/economySystem.ts +++ b/packages/game-engine/src/systems/economySystem.ts @@ -1,5 +1,5 @@ import type { GameState, EconomyState, InfrastructureState } from '@ai-tycoon/shared'; -import { FINANCIAL_SNAPSHOT_INTERVAL, MAX_FINANCIAL_HISTORY } from '@ai-tycoon/shared'; +import { FINANCIAL_SNAPSHOT_INTERVAL, MAX_FINANCIAL_HISTORY, REGULATION_COMPLIANCE_PER_CAPABILITY } from '@ai-tycoon/shared'; import type { MarketTickResult } from './marketSystem'; export function processEconomy( @@ -15,7 +15,12 @@ export function processEconomy( const talentExpenses = state.talent.totalSalaryPerTick; const dataExpenses = state.data.partnerships.reduce((sum, p) => sum + p.costPerTick, 0); - const expenses = infraExpenses + talentExpenses + dataExpenses; + + const bestCapability = state.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0); + const eraIdx = ['startup', 'scaleup', 'bigtech', 'agi'].indexOf(state.meta.currentEra); + const complianceCost = bestCapability > 30 ? bestCapability * REGULATION_COMPLIANCE_PER_CAPABILITY * (1 + eraIdx * 0.5) / 100 : 0; + + const expenses = infraExpenses + talentExpenses + dataExpenses + complianceCost; const money = state.economy.money + revenue - expenses; diff --git a/packages/game-engine/src/systems/modelSystem.ts b/packages/game-engine/src/systems/modelSystem.ts index 5e397c6..bd8e2cc 100644 --- a/packages/game-engine/src/systems/modelSystem.ts +++ b/packages/game-engine/src/systems/modelSystem.ts @@ -65,10 +65,16 @@ function createTrainedModel( speed: Math.max(1, 100 - compute * 0.5 + efficiencyBonus * 2), }; - const benchmarkScore = (capabilities.reasoning * 0.3 + capabilities.coding * 0.25 + - capabilities.creative * 0.2 + capabilities.multimodal * 0.15 + capabilities.agents * 0.1); + const safetyResearch = state.research.completedResearch.filter( + r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional'), + ).length; + const safetyScore = Math.min(100, 30 + safetyResearch * 15 + Math.random() * 10); - const safetyScore = 50 + Math.random() * 20; + const safetyPenalty = safetyScore > 60 ? (safetyScore - 60) * 0.1 : 0; + const benchmarkScore = Math.max(0, + (capabilities.reasoning * 0.3 + capabilities.coding * 0.25 + + capabilities.creative * 0.2 + capabilities.multimodal * 0.15 + capabilities.agents * 0.1) - safetyPenalty, + ); const parameterCount = Math.pow(10, generation) * (0.5 + Math.random()); diff --git a/packages/game-engine/src/systems/reputationSystem.ts b/packages/game-engine/src/systems/reputationSystem.ts index 4202f31..d9992da 100644 --- a/packages/game-engine/src/systems/reputationSystem.ts +++ b/packages/game-engine/src/systems/reputationSystem.ts @@ -1,8 +1,48 @@ import type { GameState, ReputationState } from '@ai-tycoon/shared'; -import { MAX_REPUTATION_HISTORY } from '@ai-tycoon/shared'; +import { + MAX_REPUTATION_HISTORY, + SAFETY_INCIDENT_PROBABILITY_BASE, + SAFETY_INCIDENT_REPUTATION_HIT, + LOW_SAFETY_THRESHOLD, +} from '@ai-tycoon/shared'; -export function processReputation(state: GameState): ReputationState { - const { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation; +export interface ReputationTickResult { + reputation: ReputationState; + safetyIncident: boolean; +} + +export function processReputation(state: GameState): ReputationState & { _safetyIncident?: boolean } { + let { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation; + + const bestModel = state.models.trainedModels + .filter(m => m.isDeployed) + .sort((a, b) => b.benchmarkScore - a.benchmarkScore)[0]; + + let safetyIncident = false; + if (bestModel) { + const safetyLevel = bestModel.safetyScore; + if (safetyLevel < LOW_SAFETY_THRESHOLD && state.meta.tickCount % 60 === 0) { + const incidentProb = SAFETY_INCIDENT_PROBABILITY_BASE * (LOW_SAFETY_THRESHOLD - safetyLevel); + if (Math.random() < incidentProb) { + safetyRecord = Math.max(0, safetyRecord - SAFETY_INCIDENT_REPUTATION_HIT); + publicPerception = Math.max(0, publicPerception - SAFETY_INCIDENT_REPUTATION_HIT * 0.5); + safetyIncident = true; + } + } + } + + const eraIdx = ['startup', 'scaleup', 'bigtech', 'agi'].indexOf(state.meta.currentEra); + const regulatoryPressure = eraIdx * 5; + const safetyResearchCount = state.research.completedResearch + .filter(r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional')).length; + const complianceBonus = safetyResearchCount * 8; + regulatoryStanding = Math.min(100, Math.max(0, + 50 + complianceBonus - regulatoryPressure, + )); + + const talentMorale = Object.values(state.talent.departments) + .reduce((sum, d) => sum + d.morale, 0) / 4; + employeeSatisfaction = talentMorale; const score = Math.round( safetyRecord * 0.3 + @@ -19,9 +59,15 @@ export function processReputation(state: GameState): ReputationState { } } - return { + const result: ReputationState & { _safetyIncident?: boolean } = { ...state.reputation, score, + safetyRecord, + publicPerception, + employeeSatisfaction, + regulatoryStanding, reputationHistory, }; + if (safetyIncident) result._safetyIncident = true; + return result; } diff --git a/packages/game-engine/src/tick.ts b/packages/game-engine/src/tick.ts index d2956be..60c8a42 100644 --- a/packages/game-engine/src/tick.ts +++ b/packages/game-engine/src/tick.ts @@ -73,7 +73,15 @@ export function processTick(state: GameState): Partial { }); } - const reputation = processReputation(stateWithTalent); + const reputationResult = processReputation(stateWithTalent); + const { _safetyIncident, ...reputation } = reputationResult; + if (_safetyIncident) { + notifications.push({ + title: 'Safety Incident!', + message: 'Your AI model caused a safety incident. Public trust and safety record damaged.', + type: 'danger', + }); + } const economy = processEconomy(stateWithTalent, market, infrastructure); const data = processData(stateWithTalent); const competitors = processCompetitors(stateWithTalent); diff --git a/packages/shared/src/constants/gameBalance.ts b/packages/shared/src/constants/gameBalance.ts index 1332428..1652227 100644 --- a/packages/shared/src/constants/gameBalance.ts +++ b/packages/shared/src/constants/gameBalance.ts @@ -53,3 +53,9 @@ export const FUNDING_ROUNDS = { export const OPEN_SOURCE_REPUTATION_BOOST = 8; export const OPEN_SOURCE_TALENT_ATTRACTION = 0.15; export const OPEN_SOURCE_REVENUE_PENALTY = 0.10; + +export const REGULATION_COMPLIANCE_BASE_COST = 0; +export const REGULATION_COMPLIANCE_PER_CAPABILITY = 0.5; +export const SAFETY_INCIDENT_PROBABILITY_BASE = 0.0002; +export const SAFETY_INCIDENT_REPUTATION_HIT = 15; +export const LOW_SAFETY_THRESHOLD = 40;