Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 5 additions & 82 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { CameraController, useCameraStore } from './components/CameraController'
import { FirstPersonController } from './components/FirstPersonController';
import ErrorBoundary from './components/ErrorBoundary';
import { LoadingScreen } from './components/LoadingScreen';
import { MachineData, MachineType } from './types';
import { MachineData } from './types';
import type { ForkliftData } from './components/ForkliftSystem';
import { audioManager } from './utils/audioManager';
import { gpuResourceManager } from './utils/GPUResourceManager';
Expand All @@ -21,8 +21,6 @@ import { RENDERER_TONE_MAPPING, TONE_EXPOSURE } from './constants/colorGrade';
import { useUIStore } from './stores/uiStore';
import { useGameSimulationStore } from './stores/gameSimulationStore';
import { useProductionStore } from './stores/productionStore';
import { useMaterialFlowStore } from './stores/materialFlowStore';
import { safeDivide } from './utils/typeGuards';
import { initializeSCADASync } from './store';
import { useShallow } from 'zustand/react/shallow';

Expand Down Expand Up @@ -499,84 +497,6 @@ const App: React.FC = () => {
};
}, [runtimeMode.benchmark]);

// Headless production simulation - runs regardless of camera position
// This ensures bags are counted even when ConveyorSystem isn't rendering
// PERF: Reduced from 1s to 5s interval to minimize store update cascades
//
// GAME TIME SCALING: Production now scales with gameSpeed so that
// the daily target (15,000 bags) is achievable within a game day.
// At gameSpeed=180 (default), 1 game day = 8 real minutes.
useEffect(() => {
// Base production: 12 bags/sec at productionSpeed=1.0, gameSpeed=60
// This yields ~15,000 bags/game-day at default settings (gameSpeed=180, productionSpeed~0.9)
const BAGS_PER_SECOND_BASE = 12;
const INTERVAL_SECONDS = 5;
const BAGS_PER_TICK = BAGS_PER_SECOND_BASE * INTERVAL_SECONDS;

const interval = setInterval(() => {
const store = useProductionStore.getState();
const gameStore = useGameSimulationStore.getState();

// Skip if tab is hidden or game is paused
if (!gameStore.isTabVisible) return;
if (gameStore.gameSpeed === 0) return;

// Scale by game speed: at 180x, production is 3x faster than at 60x
// This makes production happen in "game time" not "real time"
const gameSpeedFactor = gameStore.gameSpeed / 60;

// Calculate bags based on production speed and game speed
// productionSpeed is typically 0.8-1.2
const bagsThisTick = BAGS_PER_TICK * productionSpeed * gameSpeedFactor;

// Only produce if we have running machines (packers)
const runningPackerMachines = store.machines.filter(
(m) => m.type === MachineType.PACKER && (m.status === 'running' || m.status === 'warning')
);
const runningPackers = runningPackerMachines.length;

if (runningPackers > 0) {
// Scale by number of running packers (3 packers at full = 100%)
const packerScale = runningPackers / 3;

// Couple production to the material-flow simulation so silo starvation,
// jams and breakdowns visibly dent throughput. currentPackerFlowRate is
// kg/sec at the final packing stage; nominal max is the packers'
// 25 kg/sec processingRate (materialFlowStore) per running packer.
const NOMINAL_PACKER_KG_PER_SEC = 25;
const flowStore = useMaterialFlowStore.getState();
const flowRate = flowStore.currentPackerFlowRate;
const flowSimLive =
Number.isFinite(flowRate) && (flowRate > 0 || flowStore.totalMaterialProcessed > 0);

let healthFactor: number;
if (flowSimLive) {
healthFactor = Math.max(
0,
Math.min(1, safeDivide(flowRate, NOMINAL_PACKER_KG_PER_SEC * runningPackers, 1))
);
} else {
// Flow network not initialized yet: fall back to average packer
// efficiency so degraded machines still produce less than pristine ones.
const avgEfficiency = safeDivide(
runningPackerMachines.reduce((sum, m) => sum + (m.metrics.efficiency ?? 100), 0),
runningPackers * 100,
1
);
healthFactor = Math.max(0, Math.min(1, avgEfficiency));
}

const finalBags = Math.round(bagsThisTick * packerScale * healthFactor * 10) / 10;

if (finalBags > 0) {
store.incrementBagsProduced(finalBags);
}
}
}, INTERVAL_SECONDS * 1000); // Run every 5 seconds

return () => clearInterval(interval);
}, [productionSpeed]);

// Initialize SCADA system - uses same consolidated subscription
const enableSCADA = useGraphicsStore((state) => state.graphics.enableSCADA);
useEffect(() => {
Expand Down Expand Up @@ -900,7 +820,10 @@ const App: React.FC = () => {
{/* Mobile touch-to-look handler (inside Canvas for R3F access) */}
{isMobile && !fpsMode && <TouchLookHandler orbitControlsRef={orbitControlsRef} />}

<RuntimeController adaptiveEnabled={enableAdaptiveQuality} />
<RuntimeController
adaptiveEnabled={enableAdaptiveQuality}
orbitControlsRef={orbitControlsRef}
/>
</Canvas>
</ErrorBoundary>
</main>
Expand Down
159 changes: 58 additions & 101 deletions src/components/FactoryExterior.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useGameSimulationStore } from '../stores/gameSimulationStore';
import { playCritterSound } from '../utils/critterAudio';
import { HeartParticle } from './effects/HeartParticle';
import { useModelTextures } from '../utils/machineTextures';
import { useProductionStore } from '../stores/productionStore';
import {
EXTERIOR_LAYERS,
FLOOR_LAYERS,
Expand All @@ -19,12 +18,15 @@ import {
import { SITE_LAYOUT } from '../constants/siteLayout';
import { UTILITY_ASSET_DEFINITIONS } from '../constants/utilityAssets';
import { createCelestialState, sampleAtmosphere, sampleCelestial } from '../simulation/atmosphere';
import {
calculateShippingTruckState,
calculateReceivingTruckState,
} from './truckbay/useTruckPhysics';
import { positionRegistry } from '../utils/positionRegistry';
import { PROCEDURAL_TEXTURES, TREE_MATERIALS } from '../utils/sharedMaterials';
import { generateMachineORM } from '../textures';
import { shouldCheckpointOpen } from './exterior/checkpointLogic';
import {
EXTERIOR_LAMP_LENS_MATERIAL,
ExteriorLampDriver,
ExteriorLampPool,
} from './exterior/ExteriorLighting';
// OUTDOOR_MATERIALS removed - grass plane now handled by TerrainGround
import { GasStation } from './GasStationInstanced';
import {
Expand Down Expand Up @@ -3117,6 +3119,7 @@ const PathLamp: React.FC<{
style?: 'modern' | 'victorian';
}> = React.memo(({ position, style = 'modern' }) => (
<group position={position}>
<ExteriorLampPool radius={style === 'victorian' ? 5.5 : 4.8} />
{/* Pole */}
<mesh position={[0, 2, 0]} castShadow>
<cylinderGeometry args={[0.08, 0.1, 4, 8]} />
Expand All @@ -3133,9 +3136,8 @@ const PathLamp: React.FC<{
<boxGeometry args={[0.4, 0.5, 0.4]} />
<meshStandardMaterial color="#1f2937" roughness={0.5} metalness={0.3} />
</mesh>
<mesh position={[0, -0.1, 0]}>
<mesh position={[0, -0.1, 0]} material={EXTERIOR_LAMP_LENS_MATERIAL}>
<boxGeometry args={[0.3, 0.25, 0.3]} />
<meshBasicMaterial color="#fef3c7" />
</mesh>
</group>
) : (
Expand All @@ -3144,9 +3146,8 @@ const PathLamp: React.FC<{
<cylinderGeometry args={[0.2, 0.15, 0.3, 8]} />
<meshStandardMaterial color="#4b5563" roughness={0.5} metalness={0.4} />
</mesh>
<mesh position={[0, -0.1, 0]}>
<mesh position={[0, -0.1, 0]} material={EXTERIOR_LAMP_LENS_MATERIAL}>
<cylinderGeometry args={[0.12, 0.15, 0.15, 8]} />
<meshBasicMaterial color="#fef3c7" />
</mesh>
</group>
)}
Expand Down Expand Up @@ -5590,106 +5591,50 @@ const CheckpointBarrier: React.FC<{
const barrierArm2Ref = useRef<THREE.Group>(null);
const lightRef = useRef<THREE.MeshBasicMaterial>(null);
const light2Ref = useRef<THREE.MeshBasicMaterial>(null);
const openRef = useRef(false);
const dock = checkpointType ?? (position[2] > 0 ? 'shipping' : 'receiving');
const checkpointPosition = useMemo(
() => ({ x: position[0], z: position[2] }),
[position[0], position[2]]
);

// Get production speed for synchronized truck timing
const productionSpeed = useProductionStore((s) => s.productionSpeed);

// Animate the barrier arms - raise when trucks approach
useFrame((state) => {
// Follow the same live tractor and trailer poses that are rendered in the
// yard. This replaces the old duplicate clock animation, which could open a
// barrier for an imaginary truck while the visible one remained elsewhere.
useFrame((state, delta) => {
const time = state.clock.elapsedTime;
const adjustedTime = time * (productionSpeed * 0.25 + 0.2);
const CYCLE_LENGTH = 60;

// Calculate truck positions
const shippingCycle = adjustedTime % CYCLE_LENGTH;
const receivingCycle = (adjustedTime + CYCLE_LENGTH / 2) % CYCLE_LENGTH;

const shippingState = calculateShippingTruckState(shippingCycle, time);
const receivingState = calculateReceivingTruckState(receivingCycle, time);

// Checkpoint positions: shipping at z=110, receiving at z=-110
// Detect when trucks are within range of this checkpoint
const DETECTION_RANGE = 40; // Units from checkpoint to start raising
const checkpointZ = position[2];

// Determine if this checkpoint should respond to shipping or receiving trucks
const isShippingCheckpoint = checkpointType === 'shipping' || checkpointZ > 0;

let shouldRaiseInbound = false; // Barrier 1 (left side, z=+3 relative)
let shouldRaiseOutbound = false; // Barrier 2 (right side, z=-3 relative)

if (isShippingCheckpoint) {
// Shipping checkpoint at z=110
// Truck enters from z=200 (coming from positive z towards dock at z=53)
// Truck exits towards z=200 (going from dock back to road)
const truckZ = shippingState.z;

// Entering phases: truck coming from road towards dock
// 'entering' is when truck is on straight approach, 'slowing' would be deceleration
const isEntering = shippingState.phase === 'entering' || shippingState.phase === 'slowing';
// Leaving phases: truck going from dock back to road
// 'accelerating' is when truck actually passes checkpoint on the way out
const isLeaving =
shippingState.phase === 'turning_out' ||
shippingState.phase === 'accelerating' ||
shippingState.phase === 'leaving';

if (isEntering && truckZ > checkpointZ - 20 && truckZ < checkpointZ + DETECTION_RANGE) {
shouldRaiseInbound = true;
}
if (isLeaving && truckZ > checkpointZ - 20 && truckZ < checkpointZ + DETECTION_RANGE) {
shouldRaiseOutbound = true;
}
} else {
// Receiving checkpoint at z=-110
const truckZ = receivingState.z;

// Entering phases: truck coming from road (z=-200) towards dock (z=-53)
const isEntering = receivingState.phase === 'entering' || receivingState.phase === 'slowing';
// Leaving phases: truck going from dock back to road
// 'accelerating' is when truck actually passes checkpoint on the way out
const isLeaving =
receivingState.phase === 'turning_out' ||
receivingState.phase === 'accelerating' ||
receivingState.phase === 'leaving';

if (isEntering && truckZ < checkpointZ + 20 && truckZ > checkpointZ - DETECTION_RANGE) {
shouldRaiseInbound = true;
}
if (isLeaving && truckZ < checkpointZ + 20 && truckZ > checkpointZ - DETECTION_RANGE) {
shouldRaiseOutbound = true;
}
}

// Target angles: 0 = down, PI/2 = up
// Both booms raise together when truck approaches from either direction
const shouldRaiseBoth = shouldRaiseInbound || shouldRaiseOutbound;
const targetAngle1 = shouldRaiseBoth ? Math.PI / 2 : 0;
const targetAngle2 = shouldRaiseBoth ? Math.PI / 2 : 0;
openRef.current = shouldCheckpointOpen(
openRef.current,
checkpointPosition,
positionRegistry.get(`${dock}-truck-cab`),
positionRegistry.get(`${dock}-truck-trailer`)
);
const targetAngle = openRef.current ? Math.PI / 2 : 0;
const safeDelta = Math.min(Math.max(delta, 0), 0.1);

// Smooth animation for barrier 1 (faster response)
if (barrierArmRef.current) {
const currentAngle1 = barrierArmRef.current.rotation.z;
const diff1 = targetAngle1 - currentAngle1;
barrierArmRef.current.rotation.z += diff1 * 0.08;
barrierArmRef.current.rotation.z = THREE.MathUtils.damp(
barrierArmRef.current.rotation.z,
targetAngle,
5.2,
safeDelta
);
}

// Smooth animation for barrier 2
if (barrierArm2Ref.current) {
const currentAngle2 = barrierArm2Ref.current.rotation.z;
const diff2 = targetAngle2 - currentAngle2;
barrierArm2Ref.current.rotation.z += diff2 * 0.08;
barrierArm2Ref.current.rotation.z = THREE.MathUtils.damp(
barrierArm2Ref.current.rotation.z,
targetAngle,
5.2,
safeDelta
);
}

// Flashing warning lights when barrier is down (truck approaching)
const flash = Math.sin(time * 4) > 0;
const isUp1 = barrierArmRef.current && barrierArmRef.current.rotation.z > Math.PI / 4;
const isUp2 = barrierArm2Ref.current && barrierArm2Ref.current.rotation.z > Math.PI / 4;
if (lightRef.current) {
lightRef.current.color.setHex(flash && !isUp1 ? 0xff0000 : 0x440000);
lightRef.current.color.setHex(flash && openRef.current ? 0xff2b1f : 0x440500);
}
if (light2Ref.current) {
light2Ref.current.color.setHex(flash && !isUp2 ? 0xff0000 : 0x440000);
light2Ref.current.color.setHex(flash && openRef.current ? 0xff2b1f : 0x440500);
}
});

Expand Down Expand Up @@ -5801,7 +5746,12 @@ const CheckpointBarrier: React.FC<{
</mesh>

{/* Barrier arm pivot - swings inward across road */}
<group ref={barrierArmRef} position={[0, 2.9, 0]}>
<group
ref={barrierArmRef}
name={`${dock}-checkpoint-inbound-arm`}
position={[0, 2.9, 0]}
userData={{ noStaticBatch: true, dynamic: true }}
>
<mesh position={[armLength / 2, 0, 0]} castShadow>
<boxGeometry args={[armLength, 0.2, 0.2]} />
<meshStandardMaterial color="#ffffff" roughness={0.5} />
Expand Down Expand Up @@ -5844,7 +5794,13 @@ const CheckpointBarrier: React.FC<{
</mesh>

{/* Barrier arm pivot - swings inward across road (rotated 180°) */}
<group ref={barrierArm2Ref} position={[0, 2.9, 0]} rotation={[0, Math.PI, 0]}>
<group
ref={barrierArm2Ref}
name={`${dock}-checkpoint-outbound-arm`}
position={[0, 2.9, 0]}
rotation={[0, Math.PI, 0]}
userData={{ noStaticBatch: true, dynamic: true }}
>
<mesh position={[armLength / 2, 0, 0]} castShadow>
<boxGeometry args={[armLength, 0.2, 0.2]} />
<meshStandardMaterial color="#ffffff" roughness={0.5} />
Expand Down Expand Up @@ -5967,6 +5923,7 @@ export const FactoryExterior: React.FC<FactoryExteriorProps> = ({ showFactoryShe

return (
<group>
<ExteriorLampDriver />
<WaterAnimationManager />
{/* ========== EXTERIOR GRASS GROUND ========== */}
{/* DISABLED: Replaced by TerrainGround unified terrain system */}
Expand Down Expand Up @@ -7272,6 +7229,7 @@ export const FactoryExterior: React.FC<FactoryExteriorProps> = ({ showFactoryShe
].map(([x, z], i) => (
<group key={`lamp-${i}`} position={[x, 0, z]}>
<GroundBlob position={[0, 0]} scale={2.4} />
<ExteriorLampPool radius={7} />
{/* Pole */}
<mesh position={[0, 3, 0]} castShadow>
<cylinderGeometry args={[0.1, 0.15, 6, 8]} />
Expand All @@ -7283,9 +7241,8 @@ export const FactoryExterior: React.FC<FactoryExteriorProps> = ({ showFactoryShe
<meshStandardMaterial color="#263238" roughness={0.5} metalness={0.4} />
</mesh>
{/* Light bulb area */}
<mesh position={[0, 5.9, 0]}>
<mesh position={[0, 5.9, 0]} material={EXTERIOR_LAMP_LENS_MATERIAL}>
<cylinderGeometry args={[0.25, 0.35, 0.3, 8]} />
<meshBasicMaterial color="#fff9c4" />
</mesh>
</group>
))}
Expand Down
Loading