Game balance audit: wire research effects, rework capability formula, fix dead systems
CI / build-and-push (push) Successful in 32s
CI / build-and-push (push) Successful in 32s
- Create researchBonuses utility to aggregate tech tree effects into all game systems (infrastructure energy costs, compute efficiency, training speed, model capability, reputation) - Rework model capability from sqrt(compute) to 4-pillar formula (params + compute + data + research) - Make context window affect benchmarks and inference speed - Add MoE tradeoffs: 1.5x VRAM, 0.8x training speed - Enforce research point costs as a gate for unlocking research - Add real consequences to data contamination events (reputation hit, legal costs) - Scale talent costs from $0.03 to $5/tick per headcount - Scale compliance costs 100x to be meaningful - Rework competitor acquisition: cheaper but grants headcount, RP, and reputation - Remove dead code: sfxVolume, autoSaveInterval, notificationsEnabled, FAST_FORWARD_BATCH_SIZE, CHINCHILLA_OPTIMAL_RATIO Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { GameState, ComputeState, InfrastructureState } from '@ai-tycoon/shared';
|
||||
import { FLOPS_TO_TOKENS_MULTIPLIER } from '@ai-tycoon/shared';
|
||||
import type { ResearchBonuses } from './researchBonuses';
|
||||
|
||||
export interface CapacityResult {
|
||||
totalFlops: number;
|
||||
@@ -13,20 +14,19 @@ export interface CapacityResult {
|
||||
tokensPerSecondCapacity: number;
|
||||
}
|
||||
|
||||
export function computeCapacity(state: GameState, infrastructure: InfrastructureState): CapacityResult {
|
||||
export function computeCapacity(state: GameState, infrastructure: InfrastructureState, researchBonuses?: ResearchBonuses): CapacityResult {
|
||||
const { totalTrainingFlops, totalInferenceFlops, totalVramGB } = infrastructure;
|
||||
const trainingAllocation = state.compute.trainingAllocation;
|
||||
const inferenceAllocation = 1 - trainingAllocation;
|
||||
|
||||
// Training hardware can do inference at ~50% efficiency
|
||||
// Inference hardware can do training at ~30% efficiency (no NVLink, poor scaling)
|
||||
const effectiveTrainingFlops =
|
||||
totalTrainingFlops * trainingAllocation +
|
||||
totalInferenceFlops * trainingAllocation * 0.3;
|
||||
|
||||
const inferenceBoost = 1 + (researchBonuses?.tokensPerFlopBonus ?? 0) + (researchBonuses?.inferenceEfficiencyBonus ?? 0);
|
||||
const effectiveInferenceFlops =
|
||||
totalInferenceFlops * inferenceAllocation +
|
||||
totalTrainingFlops * inferenceAllocation * 0.5;
|
||||
(totalInferenceFlops * inferenceAllocation +
|
||||
totalTrainingFlops * inferenceAllocation * 0.5) * inferenceBoost;
|
||||
|
||||
const tokensPerSecondCapacity = effectiveInferenceFlops * FLOPS_TO_TOKENS_MULTIPLIER;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
estimateNetworkSlots,
|
||||
} from '@ai-tycoon/shared';
|
||||
import type { TickNotification } from '../tick';
|
||||
import type { ResearchBonuses } from './researchBonuses';
|
||||
|
||||
export interface InfraTickResult {
|
||||
infrastructure: InfrastructureState;
|
||||
@@ -463,7 +464,7 @@ function computeInterconnectMultiplier(
|
||||
|
||||
// --- Main Infrastructure Tick ---
|
||||
|
||||
export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
export function processInfrastructure(state: GameState, researchBonuses?: ResearchBonuses): InfraTickResult {
|
||||
const notifications: TickNotification[] = [];
|
||||
let repairCosts = 0;
|
||||
|
||||
@@ -587,7 +588,9 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
continue;
|
||||
}
|
||||
|
||||
const speed = stageSpeed(cohort.stage, engEff, opsEff);
|
||||
const baseSpeed = stageSpeed(cohort.stage, engEff, opsEff);
|
||||
const pipelineBonus = cohort.stage !== 'repair' ? (researchBonuses?.pipelineSpeedBonus ?? 0) : 0;
|
||||
const speed = baseSpeed * (1 + pipelineBonus);
|
||||
const newProgress = cohort.stageProgress + speed;
|
||||
|
||||
if (newProgress < cohort.stageTotal) {
|
||||
@@ -728,8 +731,9 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
}
|
||||
|
||||
const pue = COOLING_TYPE_CONFIGS[dc.coolingType].pueMultiplier;
|
||||
const energyReduction = researchBonuses?.energyCostReduction ?? 0;
|
||||
const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP)
|
||||
* location.energyCostMultiplier * pue;
|
||||
* location.energyCostMultiplier * pue * (1 - energyReduction);
|
||||
const maintenanceCostPerTick = totalRacksInDc * BASE_MAINTENANCE_PER_RACK;
|
||||
const currentUptime = totalRacksInDc > 0 ? effectiveComputeRacks / totalRacksInDc : 1;
|
||||
|
||||
|
||||
@@ -21,16 +21,21 @@ import {
|
||||
DISTILLATION_BASE_RETENTION,
|
||||
QUANTIZATION_TICKS,
|
||||
} from '@ai-tycoon/shared';
|
||||
import type { ResearchBonuses } from './researchBonuses';
|
||||
|
||||
export interface ModelTickResult {
|
||||
modelsState: ModelsState;
|
||||
completedModels: BaseModel[];
|
||||
notifications: { title: string; message: string; type: 'success' | 'warning' | 'info' }[];
|
||||
notifications: { title: string; message: string; type: 'success' | 'warning' | 'info' | 'danger' }[];
|
||||
reputationHit: number;
|
||||
legalCosts: number;
|
||||
}
|
||||
|
||||
export function processModels(state: GameState): ModelTickResult {
|
||||
export function processModels(state: GameState, researchBonuses?: ResearchBonuses): ModelTickResult {
|
||||
const completedModels: BaseModel[] = [];
|
||||
const notifications: ModelTickResult['notifications'] = [];
|
||||
let totalReputationHit = 0;
|
||||
let totalLegalCosts = 0;
|
||||
let baseModels = [...state.models.baseModels];
|
||||
let families = [...state.models.families];
|
||||
|
||||
@@ -40,7 +45,8 @@ export function processModels(state: GameState): ModelTickResult {
|
||||
state.talent.departments.research.effectiveness;
|
||||
const engineerBoost = state.talent.departments.engineering.headcount *
|
||||
state.talent.departments.engineering.effectiveness;
|
||||
const speedMultiplier = 1 + (researcherBoost + engineerBoost) * 0.05;
|
||||
const trainingResearchBonus = researchBonuses?.trainingSpeedBonus ?? 0;
|
||||
const speedMultiplier = (1 + (researcherBoost + engineerBoost) * 0.05) * (1 + trainingResearchBonus);
|
||||
|
||||
const updatedPipelines: TrainingPipeline[] = [];
|
||||
|
||||
@@ -51,7 +57,8 @@ export function processModels(state: GameState): ModelTickResult {
|
||||
}
|
||||
|
||||
const generation = families.find(f => f.id === pipeline.familyId)?.generation ?? 1;
|
||||
const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[generation] ?? 0;
|
||||
const moeVramMultiplier = pipeline.architecture.type === 'moe' ? 1.5 : 1.0;
|
||||
const requiredVram = (VRAM_REQUIREMENTS_BY_GENERATION[generation] ?? 0) * moeVramMultiplier;
|
||||
if (requiredVram > 0 && state.compute.totalVramGB < requiredVram) {
|
||||
updatedPipelines.push({ ...pipeline, status: 'stalled' });
|
||||
continue;
|
||||
@@ -62,7 +69,8 @@ export function processModels(state: GameState): ModelTickResult {
|
||||
|
||||
if (pipeline.currentStage === 'pretraining') {
|
||||
const stage = { ...pipeline.stages.pretraining };
|
||||
const newProgress = stage.progressTicks + speedMultiplier;
|
||||
const moeSpeedPenalty = pipeline.architecture.type === 'moe' ? 0.8 : 1.0;
|
||||
const newProgress = stage.progressTicks + speedMultiplier * moeSpeedPenalty;
|
||||
|
||||
const events = generateTrainingEvents(pipeline, state);
|
||||
let tickDelay = 0;
|
||||
@@ -81,7 +89,10 @@ export function processModels(state: GameState): ModelTickResult {
|
||||
tickDelay += event.impact.ticksDelayed ?? 0;
|
||||
notifications.push({ title: 'Hardware Failure', message: `${pipeline.modelName}: GPU failure during training. Recovering from checkpoint.`, type: 'warning' });
|
||||
} else if (event.type === 'data_contamination') {
|
||||
notifications.push({ title: 'Data Contamination', message: `${pipeline.modelName}: Copyright concerns detected in training data.`, type: 'warning' });
|
||||
totalReputationHit += event.impact.reputationHit ?? 0;
|
||||
totalLegalCosts += event.impact.legalCost ?? 0;
|
||||
const costStr = event.impact.legalCost ? ` Legal costs: $${(event.impact.legalCost).toLocaleString()}` : '';
|
||||
notifications.push({ title: 'Data Contamination', message: `${pipeline.modelName}: Copyright concerns detected in training data.${costStr}`, type: 'danger' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +112,7 @@ export function processModels(state: GameState): ModelTickResult {
|
||||
updated.currentStage = 'alignment';
|
||||
notifications.push({ title: 'Pre-training Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' });
|
||||
} else {
|
||||
const model = createBaseModel(updated, state);
|
||||
const model = createBaseModel(updated, state, researchBonuses);
|
||||
baseModels = [...baseModels, model];
|
||||
families = families.map(f =>
|
||||
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
|
||||
@@ -123,7 +134,7 @@ export function processModels(state: GameState): ModelTickResult {
|
||||
updated.currentStage = 'alignment';
|
||||
notifications.push({ title: 'SFT Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' });
|
||||
} else {
|
||||
const model = createBaseModel(updated, state);
|
||||
const model = createBaseModel(updated, state, researchBonuses);
|
||||
baseModels = [...baseModels, model];
|
||||
families = families.map(f =>
|
||||
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
|
||||
@@ -141,7 +152,7 @@ export function processModels(state: GameState): ModelTickResult {
|
||||
stage.isComplete = true;
|
||||
stage.progressTicks = stage.totalTicks;
|
||||
|
||||
const model = createBaseModel(updated, state);
|
||||
const model = createBaseModel(updated, state, researchBonuses);
|
||||
baseModels = [...baseModels, model];
|
||||
families = families.map(f =>
|
||||
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
|
||||
@@ -195,6 +206,8 @@ export function processModels(state: GameState): ModelTickResult {
|
||||
},
|
||||
completedModels,
|
||||
notifications,
|
||||
reputationHit: totalReputationHit,
|
||||
legalCosts: totalLegalCosts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -262,12 +275,13 @@ function generateTrainingEvents(pipeline: TrainingPipeline, state: GameState): T
|
||||
? state.data.ownedDatasets.reduce((sum, d) => sum + d.legalRisk, 0) / state.data.ownedDatasets.length
|
||||
: 0;
|
||||
if (Math.random() < baseProbability * (hasDataPipeline ? 0.25 : 0.5) * avgLegalRisk) {
|
||||
const legalCost = 5000 + Math.floor(Math.random() * 15000);
|
||||
events.push({
|
||||
id: uuid(), type: 'data_contamination', tick: state.meta.tickCount,
|
||||
severity: 'moderate',
|
||||
severity: avgLegalRisk > 0.4 ? 'major' : 'moderate',
|
||||
description: 'Copyright holders identified content in training data.',
|
||||
resolved: true,
|
||||
impact: {},
|
||||
impact: { reputationHit: 5, legalCost },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -288,36 +302,61 @@ function generateTrainingEvents(pipeline: TrainingPipeline, state: GameState): T
|
||||
function createBaseModel(
|
||||
pipeline: TrainingPipeline,
|
||||
state: GameState,
|
||||
researchBonuses?: ResearchBonuses,
|
||||
): BaseModel {
|
||||
const { architecture, dataMix } = pipeline;
|
||||
const compute = pipeline.stages.pretraining.computeAllocated;
|
||||
const dataTokens = pipeline.stages.pretraining.targetTokens;
|
||||
const params = architecture.totalParameters;
|
||||
|
||||
const computeFactor = Math.sqrt(compute) * 5;
|
||||
const dataFactor = Math.log10(1 + dataTokens / 1e8) * 10;
|
||||
const researchBonus = state.research.completedResearch.length * 3;
|
||||
const efficiencyBonus = state.research.completedResearch.filter(r => r.includes('efficiency')).length * 5;
|
||||
// Pillar 1: Parameters (0-30) — larger models have higher ceiling
|
||||
const paramFactor = Math.min(30, Math.log2(1 + params) * 4.5);
|
||||
|
||||
let rawCapability = Math.min(95, computeFactor + dataFactor + researchBonus + efficiencyBonus);
|
||||
// Pillar 2: Compute (0-25) — compute relative to parameter count (Chinchilla scaling)
|
||||
const computePerParam = compute / Math.max(1, params);
|
||||
const computeFactor = Math.min(25, Math.sqrt(computePerParam) * 8);
|
||||
|
||||
// Pillar 3: Data (0-20) — token count with quality multiplier
|
||||
const dataQualityMultiplier = 1 + (researchBonuses?.dataQualityBonus ?? 0);
|
||||
const dataFactor = Math.min(20, Math.log10(1 + dataTokens / 1e8) * 8 * dataQualityMultiplier);
|
||||
|
||||
// Pillar 4: Research (0-20) — accumulated research knowledge
|
||||
const capabilityResearchBonus = researchBonuses?.globalCapabilityBonus ?? 0;
|
||||
const researchFactor = Math.min(20, capabilityResearchBonus + state.research.completedResearch.length * 0.5);
|
||||
|
||||
let rawCapability = Math.min(95, paramFactor + computeFactor + dataFactor + researchFactor);
|
||||
|
||||
if (architecture.type === 'moe') {
|
||||
rawCapability = Math.min(98, rawCapability * MOE_CAPABILITY_MULTIPLIER);
|
||||
}
|
||||
|
||||
// MoE tradeoff: total params need full VRAM even though only active params run
|
||||
// This is enforced in the UI/store when checking VRAM requirements
|
||||
|
||||
const researcherQuality = state.talent.departments.research.effectiveness;
|
||||
const contextBonus = Math.log2(Math.max(1, architecture.contextWindow / 4)) * 3;
|
||||
const contextPenalty = Math.max(0, Math.log2(architecture.contextWindow / 8)) * 2;
|
||||
|
||||
const capabilities: ModelCapabilities = {
|
||||
reasoning: clamp(rawCapability * (0.6 + dataMix.scientific * 0.5 + dataMix.code * 0.3) * (1 + researcherQuality * 0.2)),
|
||||
coding: clamp(rawCapability * (0.5 + dataMix.code * 1.0)),
|
||||
creative: clamp(rawCapability * (0.4 + dataMix.books * 0.6 + dataMix.conversation * 0.3)),
|
||||
math: clamp(rawCapability * (0.3 + dataMix.scientific * 0.7 + dataMix.code * 0.2)),
|
||||
knowledge: clamp(rawCapability * (0.5 + dataMix.web * 0.3 + dataMix.books * 0.3)),
|
||||
knowledge: clamp(rawCapability * (0.5 + dataMix.web * 0.3 + dataMix.books * 0.3) + contextBonus * 0.3),
|
||||
multimodal: clamp(rawCapability * (dataMix.images * 0.5 + dataMix.video * 0.4 + dataMix.audio * 0.2)),
|
||||
agents: clamp(rawCapability * (0.2 + dataMix.code * 0.3 + dataMix.conversation * 0.2)),
|
||||
speed: Math.max(1, 100 - architecture.totalParameters * 0.3 + efficiencyBonus * 2 + (architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER * 10 : 0)),
|
||||
agents: clamp(rawCapability * (0.2 + dataMix.code * 0.3 + dataMix.conversation * 0.2) + contextBonus * 0.5),
|
||||
speed: Math.max(1, 100 - params * 0.3 - contextPenalty + (researchBonuses?.inferenceEfficiencyBonus ?? 0) * 20 + (architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER * 10 : 0)),
|
||||
contextUtilization: Math.min(100, architecture.contextWindow * 0.4),
|
||||
};
|
||||
|
||||
if (researchBonuses) {
|
||||
capabilities.reasoning = clamp(capabilities.reasoning + researchBonuses.reasoningBonus);
|
||||
capabilities.coding = clamp(capabilities.coding + researchBonuses.codingBonus);
|
||||
capabilities.creative = clamp(capabilities.creative + researchBonuses.creativeBonus);
|
||||
capabilities.multimodal = clamp(capabilities.multimodal + researchBonuses.multimodalBonus);
|
||||
capabilities.agents = clamp(capabilities.agents + researchBonuses.agentsBonus);
|
||||
}
|
||||
|
||||
const breakthroughBonuses: Partial<Record<keyof ModelCapabilities, number>> = {};
|
||||
for (const event of pipeline.events) {
|
||||
if ((event.type === 'breakthrough' || event.type === 'emergent_capability') && event.impact.capabilityDomain && event.impact.capabilityBonus) {
|
||||
@@ -347,10 +386,8 @@ function createBaseModel(
|
||||
}
|
||||
}
|
||||
|
||||
const safetyResearch = state.research.completedResearch.filter(
|
||||
r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional'),
|
||||
).length;
|
||||
let overallSafety = Math.min(100, 30 + safetyResearch * 15 + Math.random() * 10);
|
||||
const safetyResearchBonus = researchBonuses?.safetyBonus ?? 0;
|
||||
let overallSafety = Math.min(100, 30 + safetyResearchBonus + Math.random() * 10);
|
||||
let refusalRate = overallSafety > 60 ? 0.1 : 0.03;
|
||||
|
||||
if (pipeline.stages.alignment?.isComplete) {
|
||||
|
||||
@@ -5,13 +5,14 @@ import {
|
||||
SAFETY_INCIDENT_REPUTATION_HIT,
|
||||
LOW_SAFETY_THRESHOLD,
|
||||
} from '@ai-tycoon/shared';
|
||||
import type { ResearchBonuses } from './researchBonuses';
|
||||
|
||||
export interface ReputationTickResult {
|
||||
reputation: ReputationState;
|
||||
safetyIncident: boolean;
|
||||
}
|
||||
|
||||
export function processReputation(state: GameState): ReputationState & { _safetyIncident?: boolean } {
|
||||
export function processReputation(state: GameState, researchBonuses?: ResearchBonuses): ReputationState & { _safetyIncident?: boolean } {
|
||||
let { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation;
|
||||
|
||||
let safetyIncident = false;
|
||||
@@ -40,6 +41,9 @@ export function processReputation(state: GameState): ReputationState & { _safety
|
||||
.reduce((sum, d) => sum + d.morale, 0) / 4;
|
||||
employeeSatisfaction = talentMorale;
|
||||
|
||||
const reputationResearchBonus = researchBonuses?.reputationBonus ?? 0;
|
||||
publicPerception = Math.min(100, publicPerception + reputationResearchBonus * 0.1);
|
||||
|
||||
const score = Math.round(
|
||||
safetyRecord * 0.3 +
|
||||
publicPerception * 0.3 +
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { TECH_TREE } from '../data/techTree';
|
||||
|
||||
export interface ResearchBonuses {
|
||||
energyCostReduction: number;
|
||||
pipelineSpeedBonus: number;
|
||||
trainingSpeedBonus: number;
|
||||
inferenceEfficiencyBonus: number;
|
||||
tokensPerFlopBonus: number;
|
||||
dataQualityBonus: number;
|
||||
sdkCoverageBonus: number;
|
||||
|
||||
globalCapabilityBonus: number;
|
||||
reasoningBonus: number;
|
||||
codingBonus: number;
|
||||
creativeBonus: number;
|
||||
multimodalBonus: number;
|
||||
agentsBonus: number;
|
||||
reputationBonus: number;
|
||||
|
||||
safetyBonus: number;
|
||||
}
|
||||
|
||||
export function getResearchBonuses(completedResearch: string[]): ResearchBonuses {
|
||||
const bonuses: ResearchBonuses = {
|
||||
energyCostReduction: 0,
|
||||
pipelineSpeedBonus: 0,
|
||||
trainingSpeedBonus: 0,
|
||||
inferenceEfficiencyBonus: 0,
|
||||
tokensPerFlopBonus: 0,
|
||||
dataQualityBonus: 0,
|
||||
sdkCoverageBonus: 0,
|
||||
globalCapabilityBonus: 0,
|
||||
reasoningBonus: 0,
|
||||
codingBonus: 0,
|
||||
creativeBonus: 0,
|
||||
multimodalBonus: 0,
|
||||
agentsBonus: 0,
|
||||
reputationBonus: 0,
|
||||
safetyBonus: 0,
|
||||
};
|
||||
|
||||
for (const id of completedResearch) {
|
||||
const node = TECH_TREE.find(n => n.id === id);
|
||||
if (!node) continue;
|
||||
|
||||
for (const effect of node.effects) {
|
||||
switch (effect.type) {
|
||||
case 'efficiency_boost':
|
||||
switch (effect.target) {
|
||||
case 'training_speed': bonuses.trainingSpeedBonus += effect.value; break;
|
||||
case 'inference': bonuses.inferenceEfficiencyBonus += effect.value; break;
|
||||
case 'tokens_per_flop': bonuses.tokensPerFlopBonus += effect.value; break;
|
||||
case 'pipeline_speed': bonuses.pipelineSpeedBonus += effect.value; break;
|
||||
case 'data_quality': bonuses.dataQualityBonus += effect.value; break;
|
||||
case 'sdk_coverage': bonuses.sdkCoverageBonus += effect.value; break;
|
||||
}
|
||||
break;
|
||||
case 'capability_boost':
|
||||
switch (effect.target) {
|
||||
case 'all': bonuses.globalCapabilityBonus += effect.value; break;
|
||||
case 'reasoning': bonuses.reasoningBonus += effect.value; break;
|
||||
case 'coding': bonuses.codingBonus += effect.value; break;
|
||||
case 'creative': bonuses.creativeBonus += effect.value; break;
|
||||
case 'multimodal': bonuses.multimodalBonus += effect.value; break;
|
||||
case 'agents': bonuses.agentsBonus += effect.value; break;
|
||||
case 'reputation': bonuses.reputationBonus += effect.value; break;
|
||||
}
|
||||
break;
|
||||
case 'cost_reduction':
|
||||
if (effect.target === 'energy') bonuses.energyCostReduction += effect.value;
|
||||
break;
|
||||
case 'safety_boost':
|
||||
bonuses.safetyBonus += effect.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bonuses;
|
||||
}
|
||||
@@ -46,6 +46,7 @@ export function getAvailableResearch(state: GameState): typeof TECH_TREE {
|
||||
if (state.research.activeResearch?.researchId === node.id) return false;
|
||||
if (eraOrder.indexOf(node.era) > currentEraIdx) return false;
|
||||
if (node.prerequisites.some(p => !state.research.completedResearch.includes(p))) return false;
|
||||
if (node.cost.researchPoints > state.research.researchPoints) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GameState, TalentState } from '@ai-tycoon/shared';
|
||||
|
||||
const SALARY_PER_HEADCOUNT_PER_TICK = 0.03;
|
||||
const SALARY_PER_HEADCOUNT_PER_TICK = 5;
|
||||
|
||||
export function processTalent(state: GameState): TalentState {
|
||||
const departments = { ...state.talent.departments };
|
||||
@@ -8,11 +8,11 @@ export function processTalent(state: GameState): TalentState {
|
||||
let totalSalary = 0;
|
||||
for (const [id, dept] of Object.entries(departments)) {
|
||||
totalSalary += dept.headcount * SALARY_PER_HEADCOUNT_PER_TICK;
|
||||
totalSalary += dept.budget / 2592000;
|
||||
totalSalary += dept.budget * 0.01;
|
||||
}
|
||||
|
||||
for (const hire of state.talent.keyHires) {
|
||||
totalSalary += hire.salary / 2592000;
|
||||
totalSalary += hire.salary;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user