Files
AIHostingTycoon/apps/web/src/components/dev/EventTriggersTab.tsx
T
josh 09a5cb69a7
CI / build-and-push (push) Successful in 42s
Overhaul market system with shared TAM competition, multi-tier pricing, enterprise pipeline, and developer ecosystem
Replaces the simplified single-subscriber market with a full competitive simulation:
shared TAM with softmax market shares across 4 segments, multi-tier consumer
subscriptions (Free/Plus/Pro/Team) and API tiers (Free/PAYG/Scale/Enterprise),
enterprise sales pipeline (Lead→Qualification→POC→Negotiation→Active→Renewal)
with SLA tracking, developer ecosystem flywheel, technology obsolescence pressure,
seasonal demand cycles, and two new product lines (Code Assistant, AI Agents Platform).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-25 08:30:24 -04:00

195 lines
7.0 KiB
TypeScript

import { useState } from 'react';
import { useGameStore } from '@/store';
import type { FundingRoundType } from '@ai-tycoon/shared';
function DevButton({ onClick, children, variant = 'default' }: {
onClick: () => void;
children: React.ReactNode;
variant?: 'default' | 'success' | 'danger' | 'warning';
}) {
const cls = {
default: 'bg-surface-800 hover:bg-surface-700 border-surface-600 text-surface-300',
success: 'bg-emerald-500/20 hover:bg-emerald-500/30 border-emerald-500/50 text-emerald-400',
danger: 'bg-red-500/20 hover:bg-red-500/30 border-red-500/50 text-red-400',
warning: 'bg-amber-500/20 hover:bg-amber-500/30 border-amber-500/50 text-amber-400',
}[variant];
return (
<button onClick={onClick} className={`px-2 py-1 rounded text-xs border ${cls}`}>
{children}
</button>
);
}
function triggerSafetyIncident() {
useGameStore.setState((s) => ({
reputation: {
...s.reputation,
safetyRecord: Math.max(0, s.reputation.safetyRecord - 15),
publicPerception: Math.max(0, s.reputation.publicPerception - 7.5),
},
}));
useGameStore.getState().addNotification({
title: '[DEV] Safety Incident',
message: 'Manually triggered safety incident. Safety -15, Public -7.5.',
type: 'danger',
tick: useGameStore.getState().meta.tickCount,
});
}
function triggerRackFailure(count: number) {
useGameStore.setState((s) => {
let remaining = count;
const newClusters = s.infrastructure.clusters.map((cluster) => ({
...cluster,
campuses: cluster.campuses.map((campus) => ({
...campus,
dataCenters: campus.dataCenters.map((dc) => {
if (remaining <= 0 || dc.computeRacksOnline <= 0) return dc;
const toFail = Math.min(remaining, dc.computeRacksOnline);
remaining -= toFail;
return {
...dc,
computeRacksOnline: dc.computeRacksOnline - toFail,
computeRacksFailed: dc.computeRacksFailed + toFail,
};
}),
})),
}));
return { infrastructure: { ...s.infrastructure, clusters: newClusters } };
});
useGameStore.getState().addNotification({
title: '[DEV] Rack Failure',
message: `Manually failed ${count} racks across data centers.`,
type: 'warning',
tick: useGameStore.getState().meta.tickCount,
});
}
function resetRackFailures() {
useGameStore.setState((s) => {
const newClusters = s.infrastructure.clusters.map((cluster) => ({
...cluster,
campuses: cluster.campuses.map((campus) => ({
...campus,
dataCenters: campus.dataCenters.map((dc) => {
const repairCount = dc.deploymentCohorts.filter(c => c.stage === 'repair').reduce((sum, c) => sum + c.count, 0);
return {
...dc,
computeRacksOnline: dc.computeRacksOnline + repairCount,
computeRacksFailed: 0,
deploymentCohorts: dc.deploymentCohorts.filter(c => c.stage !== 'repair'),
};
}),
})),
}));
return { infrastructure: { ...s.infrastructure, clusters: newClusters } };
});
}
function triggerMarketBoom(multiplier: number) {
useGameStore.setState((s) => ({
market: {
...s.market,
consumerTiers: {
...s.market.consumerTiers,
totalUsers: Math.round(s.market.consumerTiers.totalUsers * multiplier),
},
},
}));
useGameStore.getState().addNotification({
title: '[DEV] Market Boom',
message: `Subscribers multiplied by ${multiplier}x.`,
type: 'success',
tick: useGameStore.getState().meta.tickCount,
});
}
function forceFunding(roundType: FundingRoundType) {
useGameStore.getState().raiseFunding(roundType);
useGameStore.getState().addNotification({
title: '[DEV] Funding',
message: `Force-raised ${roundType} funding round.`,
type: 'success',
tick: useGameStore.getState().meta.tickCount,
});
}
export function EventTriggersTab() {
const [failCount, setFailCount] = useState('10');
const [boomMultiplier, setBoomMultiplier] = useState('2');
const completedRounds = useGameStore((s) => s.economy.funding.completedRounds);
const totalFailedRacks = useGameStore((s) =>
s.infrastructure.clusters.reduce((sum, cl) =>
sum + cl.campuses.reduce((s2, ca) =>
s2 + ca.dataCenters.reduce((s3, dc) => s3 + dc.computeRacksFailed, 0), 0), 0));
const fundingRounds: { type: FundingRoundType; label: string }[] = [
{ type: 'seed', label: 'Seed ($500K)' },
{ type: 'seriesA', label: 'A ($2M)' },
{ type: 'seriesB', label: 'B ($10M)' },
{ type: 'seriesC', label: 'C ($50M)' },
{ type: 'seriesD', label: 'D ($200M)' },
{ type: 'ipo', label: 'IPO ($1B)' },
];
const completedTypes = new Set(completedRounds.map((r) => r.type));
return (
<div className="space-y-4">
<div>
<div className="text-[10px] uppercase tracking-wider text-surface-500 mb-1.5 font-semibold">Reputation Events</div>
<DevButton onClick={triggerSafetyIncident} variant="danger">Trigger Safety Incident</DevButton>
</div>
<div>
<div className="text-[10px] uppercase tracking-wider text-surface-500 mb-1.5 font-semibold">
Infrastructure Events {totalFailedRacks > 0 && <span className="text-red-400">({totalFailedRacks} failed)</span>}
</div>
<div className="flex gap-1.5 items-center">
<input
type="number"
value={failCount}
onChange={(e) => setFailCount(e.target.value)}
className="bg-surface-800 border border-surface-600 rounded px-2 py-1 text-xs text-surface-100 w-16"
min={1}
/>
<DevButton onClick={() => triggerRackFailure(Number(failCount) || 10)} variant="danger">Fail Racks</DevButton>
<DevButton onClick={resetRackFailures} variant="success">Reset All Failures</DevButton>
</div>
</div>
<div>
<div className="text-[10px] uppercase tracking-wider text-surface-500 mb-1.5 font-semibold">Market Events</div>
<div className="flex gap-1.5 items-center">
<DevButton onClick={() => triggerMarketBoom(Number(boomMultiplier) || 2)} variant="warning">Market Boom</DevButton>
<span className="text-xs text-surface-400">x</span>
<input
type="number"
value={boomMultiplier}
onChange={(e) => setBoomMultiplier(e.target.value)}
className="bg-surface-800 border border-surface-600 rounded px-2 py-1 text-xs text-surface-100 w-12"
min={1}
step={0.5}
/>
</div>
</div>
<div>
<div className="text-[10px] uppercase tracking-wider text-surface-500 mb-1.5 font-semibold">Force Funding</div>
<div className="flex flex-wrap gap-1.5">
{fundingRounds.map(({ type, label }) => (
<DevButton
key={type}
onClick={() => forceFunding(type)}
variant={completedTypes.has(type) ? 'default' : 'success'}
>
{label} {completedTypes.has(type) && '✓'}
</DevButton>
))}
</div>
</div>
</div>
);
}