Skip to main content
PulseScanner.io

Address

0x9eb00a1e41159f9a938003369080d1e40200ffce
Current Holdings
$0.8957
TXs sent
not counted
First Active
2026-03-23
block 26,098,278
Last Active
40 days ago
block 27,212,572
Funded By
0xdf56…c86c

Net worth historyi

27 snapshots · to block 27,212,599
exact matchGAME_CONTRACTsolc 0.8.20+commit.a1b79de6runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Contract Name: GAME_CONTRACT

/*
============================================================
PEWPEW RACING SYSTEM — CONTRACTS OVERVIEW
============================================================

1. UPGRADE_REGISTRY
    - Canonical source of all upgrade definitions
    - Defines slots, stats, pricing, and paint upgrades

2. RACER_UPGRADES_1155
    - ERC1155 inventory holding all upgrade tokens
    - Validates upgrade IDs against UpgradeRegistry
    - Enforces mint/burn permissions

3. CAR_NFT
    - ERC721 representing player-owned cars
    - Stores base car identity and ownership

4. GAME_CONTRACT (THIS CONTRACT)
    - Core gameplay logic
    - Applies upgrades, runs races, enforces rules

5. GAME_VIEWS (GAME_VIEWS.sol)
    - Visuals helper (side/top manifests)
    - Aux storage + helpers to keep GAME_CONTRACT under the 24KB code-size limit
      (stores replays; hosts swap helper + burn-selection helper)
    - Only the configured GAME_CONTRACT can write (set via `setGame()`)

6. UPGRADE_SHOP
    - Handles purchasing and minting of upgrades
    - Only contract allowed to mint upgrade tokens

7. CAR_SHOP
    - Handles car minting and acquisition
    - Entry point for new players
*/

interface IERC20 {
    function balanceOf(address who) external view returns (uint256);
}

interface IERC20Burnable {
    function burn(uint256 amount) external;
}

interface IPulseXRouterV2 {
    function swapExactETHForTokens(
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external payable returns (uint[] memory amounts);

    function WPLS() external pure returns (address);
}

interface IRaceCarNFT {
    function ownerOf(uint256 tokenId) external view returns (address);

    function cars(uint256 tokenId)
        external
        view
        returns (
            uint32 modelId,
            uint8 rarity,
            uint16 baseSpeed,
            uint16 baseHandling,
            uint16 baseAccel
        );
}

interface IUpgradeRegistry {
    function slotCount() external view returns (uint8);
    function exists(uint256 id) external view returns (bool);
    function getStats(uint256 id) external view returns (int16 speed, int16 accel, int16 handling);
    function getLevel(uint256 id) external view returns (uint8);
    function getSlotType(uint256 id) external view returns (uint8);
}

interface IRacerUpgrades1155GameBurn {
    function balanceOf(address account, uint256 id) external view returns (uint256);
    function burnForGame(address from, uint256 id, uint256 amount) external;
}

interface IGAME_AUX {
    function getSwapPath(uint8 mode) external view returns (address[] memory path);

    function setSwapPath(uint8 mode, address[] calldata path) external;

    function swapExactPlsForMode(uint8 mode, address router, uint16 slippageBps)
        external
        payable
        returns (uint256 outReceived);

    function storeDragRaceReplay(
        uint256 raceId,
        uint256 car1,
        uint256 car2,
        uint256 time1,
        uint256 time2,
        uint256 winnerCarId,
        address winnerOwner,
        address starter,
        uint64 timestamp,
        uint256 rewardPaid
    ) external;

    function getDragRaceReplay(uint256 raceId)
        external
        view
        returns (
            uint256 car1,
            uint256 car2,
            uint256 time1,
            uint256 time2,
            uint256 winnerCarId,
            address winnerOwner,
            address starter,
            uint64 timestamp,
            uint256 rewardPaid
        );

    function storeSingleRaceBatchReplay(
        uint256 raceId,
        uint8 trackId,
        uint256[] calldata cars,
        uint256[] calldata times,
        uint256 winnerCarId,
        address winnerOwner,
        uint256 winnerTime,
        address starter,
        uint64 timestamp,
        uint256 rewardPaid
    ) external;

    function getSingleRaceBatchReplay(uint256 raceId)
        external
        view
        returns (
            uint8 trackId,
            uint8 carCount,
            uint256 winnerCarId,
            address winnerOwner,
            uint256 winnerTime,
            uint64 timestamp,
            address starter,
            uint256 rewardPaid
        );

    function getSingleRaceBatchCars(uint256 raceId) external view returns (uint256[] memory);
    function getSingleRaceBatchTimes(uint256 raceId) external view returns (uint256[] memory);

    function storeSeasonLeg(
        uint256 seasonId,
        uint8 leg,
        uint8 trackId,
        uint256 winnerCarId,
        uint256 winnerTime,
        uint256 racedAt
    ) external;

    function getSeasonLeg(uint256 seasonId, uint8 leg)
        external
        view
        returns (uint8 trackId, uint256 winnerCarId, uint256 winnerTime, uint256 racedAt);

    function storeSeasonFinal(uint256 seasonId, uint256 winnerCarId, address winnerOwner, uint256 prizePaid) external;
    function getSeasonFinal(uint256 seasonId) external view returns (uint256 winnerCarId, address winnerOwner, uint256 prizePaid);

    function purgeSeason(uint256 seasonId) external;

    function pickRandomHighestUpgrade(address player, uint256 carId) external view returns (uint256 upgradeId);
    function pickRandomEquippedToken(address player, uint256 carId) external view returns (uint256 upgradeId);

    function validateNoZeroNoDups(uint256[] calldata ids) external pure;
}

contract GAME_CONTRACT {
    // -----------------------
    // Custom errors (bytecode shrink)
    // -----------------------
    error Reentrancy();

    error Paused();

    error NotOwner();
    error NewOwnerZero();
    error TooHigh();

    error MaxTracks();
    error InvalidTrack();

    error ToZero();
    error TransferFailed();
    error PlsSendFailed();

    error VaultZero();
    error NeedPewpew();

    error NotCarOwner();
    error InvalidUpgrade();
    error NoToken();
    error SlotTooLarge();
    error NothingEquipped();

    error WrongPlsAmount();
    error SameCar();
    error StarterMustOwnCar1();

    error Need2Cars();
    error TooManyCars();
    error StarterMustOwnFirstCar();


    error SeasonInactive();
    error SeasonComplete();
    error NoRoster();
    error CallerPewpewTransferFailed();

    error NotActive();

    error StarterCarMismatch();
    error BurnGreaterThanReceived();

    error RewardTransferFailed();

    error Need1MPewpew();

    error SeasonLimit();

    // V2
    error InvalidRandomnessRange();
    error SeasonLegCooldown();
    error HelperNotSet();
    error ViewsCallFailed();

    // Selectors for external views helper
    bytes4 private constant _SEL_GET_SIDE_VISUALS = 0xda4c4cf9; // getSideViewVisuals(address,uint256)
    bytes4 private constant _SEL_GET_TOP_VISUALS = 0x81571583;  // getTopViewVisuals(address,uint256)

    // -----------------------
    // Constants
    // -----------------------
    uint8 internal constant MAX_TRACKS = 55;
    uint8 internal constant MAX_SLOTS = 64;
    uint16 internal constant MAX_BPS = 10_000;

    uint256 internal constant MAX_RACE_CARS = 32;
    uint256 internal constant MAX_SINGLE_RACE_CARS = 20;
    uint8 internal constant SEASON_LEGS = 5;

    // -----------------------
    // Reentrancy guard (no OZ)
    // -----------------------
    uint256 private _reentrancyStatus = 1;

    modifier nonReentrant() {
        if (_reentrancyStatus != 1) revert Reentrancy();
        _reentrancyStatus = 2;
        _;
        _reentrancyStatus = 1;
    }

    // -----------------------
    // Pause (races)
    // -----------------------
    bool public paused;
    event PausedSet(bool paused);

    modifier whenNotPaused() {
        if (paused) revert Paused();
        _;
    }

    // -----------------------
    // Admin
    // -----------------------
    address internal owner;
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    // -----------------------
    // External contracts
    // -----------------------
    IERC20 internal pewpew; // 18 decimals
    IERC20 internal usdc;   // 6 decimals
    IERC20 internal pewswap5; // 18 decimals

    IPulseXRouterV2 internal router;

    IRaceCarNFT public carNFT;
    IUpgradeRegistry public upgradeRegistry;
    IRacerUpgrades1155GameBurn public upgrades1155;

    // -----------------------
    // Slippage config (PLS swaps)
    // -----------------------
    uint16 internal slippageBps = 300; // 3%

    // -----------------------
    // Vault defaults
    // -----------------------
    address internal constant DEFAULT_FARMERS_LPUSDC_VAULT =
        0xe046d5921f68B7fB305e5c55269163E6e217a3b8;

    address internal constant DEFAULT_BENEVOLENCE_VAULT =
        0x6ee09dACF727E423A05568365C04664A0cAe171f;

    address internal benevolenceVault;
    address internal lpHarvesterVault;

    // -----------------------
    // Vault send (PEWPEW-gated)
    // -----------------------
    // Owner-configurable minimum PEWPEW required to call `holderSendToVault()`.
    // Default: 1,000,000 PEWPEW.
    uint256 public minPewpewToVaultBlast = 1_000_000 ether;

    uint256 internal maxPewpewVaultSend = 5_000 ether;
    uint256[5] internal vaultBlastTierThresholds = [
        uint256(1_000_000 ether),
        5_000_000 ether,
        10_000_000 ether,
        20_000_000 ether,
        30_000_000 ether
    ];
    uint16[5] internal vaultBlastTierRewardBps = [uint16(100), 200, 300, 400, 500];

    // -----------------------
    // Tracks
    // -----------------------
    uint8 public trackCount;

    struct Track {
        uint16 speedMult;    // 1000 = 1.0x
        uint16 accelMult;    // 1000 = 1.0x
        uint16 handlingMult; // 1000 = 1.0x
    }

    mapping(uint8 => Track) public tracks;

    // V2: per-track randomness range (bps). If both are zero, no upset is applied.
    mapping(uint8 => uint16) internal upsetChanceMinBps;
    mapping(uint8 => uint16) internal upsetChanceMaxBps;

    // -----------------------
    // Upgrades (equip state)
    // -----------------------
    mapping(uint256 => mapping(uint256 => uint256)) public equippedUpgradeByCarSlot;

    // -----------------------
    // Drag best times
    // -----------------------
    mapping(uint256 => uint256) public bestDragTimeMsByCar;

    // -----------------------
    // V2: Drag race replay storage
    // -----------------------
    uint256 internal nextDragRaceId = 1;

    // Replay data is stored in the external `aux` helper.

    // -----------------------
    // Seasons (aggregate: 5 legs)
    // -----------------------
    uint256 internal nextSeasonId = 1;

    mapping(uint256 => mapping(uint256 => uint256)) public seasonTotalTimeMs;

    uint256[] internal activeSeasons;
    mapping(uint256 => bool) public isSeasonActive;

    mapping(uint256 => uint8) public seasonRaceCount; // 0..5

    mapping(uint256 => uint256[]) private _seasonCars;

    // -----------------------
    // Best track times
    // -----------------------
    mapping(uint256 => mapping(uint8 => uint256)) public bestTrackTimeMsByCarTrack;

    // -----------------------
    // Season randomness (instant hybrid entropy)
    // -----------------------
    uint256 internal seasonEntropyAccumulator;
    mapping(uint256 => uint256) internal seasonRaceNonce;

    // -----------------------
    // Season configuration
    // -----------------------
    uint16 public seasonBurnBps = 500; // 5%
    uint16 public seasonChampionRewardBps = 2000; // 20% of PEWSWAP5 balance at finalize

    // Blast caller tiers (PEWPEW balance thresholds) for calling `blastSeasonRace`.
    // Tiers: 1m / 5m / 10m / 20m / 30m.
    uint256 internal constant SEASON_BLAST_TIER1 = 1_000_000 ether;
    uint256 internal constant SEASON_BLAST_TIER2 = 5_000_000 ether;
    uint256 internal constant SEASON_BLAST_TIER3 = 10_000_000 ether;
    uint256 internal constant SEASON_BLAST_TIER4 = 20_000_000 ether;
    uint256 internal constant SEASON_BLAST_TIER5 = 30_000_000 ether;

    // Reward bps (of current contract PEWPEW balance at call time) for tiers 1..5.
    // Defaults: 1% / 2% / 3% / 4% / 5%.
    uint16[5] internal seasonBlastTierRewardBps = [uint16(100), 200, 300, 400, 500];

    // Minimum PEWPEW required to call season blast (legs 2..5). Default: 1,000,000 PEWPEW.
    uint256 public minPewpewToSeasonBlast = SEASON_BLAST_TIER1;

    // V2: season leg cooldown (for legs 2..5)
    uint32 internal seasonLegCooldownSeconds = 2 minutes;
    mapping(uint256 => uint256) internal seasonNextLegAvailableAt;

    // V2: owner-configured 5-track season sequence
    uint8[SEASON_LEGS] internal seasonIncludedTracks;

    // V2: per-leg stored results for replay are stored in the external `aux` helper.

    // V2: optional season history purge to reduce long-term storage growth
    uint256 internal seasonHistoryLimit = 20; // set to 0 to disable purging
    uint256[] internal seasonHistory; // ended seasons in order

    // -----------------------
    // Fees (exact PLS required) — paid modes
    // -----------------------
    uint256 public ghostRaceFee = 200 ether;
    uint256 public dragRaceFee = 400 ether;
    uint256 public singleRaceFee = 800 ether;
    uint256 public seasonStartFee = 100_000 ether;

    // Starter win payouts (of the PEWSWAP5 received from the fee swap)
    uint16 public dragStarterWinRewardBps = 2500;   // 25%
    uint16 public singleStarterWinRewardBps = 3500; // 35%

    // -----------------------
    // V2: Single race (multi-car) replay storage
    // -----------------------
    uint256 internal nextSingleRaceBatchId = 1;

    // Replay data is stored in the external `aux` helper.

    // -----------------------
    // Events (gameplay)
    // -----------------------
    event DragRaceResult(uint256 car1, uint256 time1, uint256 car2, uint256 time2, address starter, bool starterWon);
    event GhostRaceResult(uint256 carId, uint256 newTime, uint256 bestTimeBefore);

    event SeasonStarted(
        uint256 indexed seasonId,
        address indexed scheduler,
        uint256 indexed starterCarId,
        uint256 rosterSize,
        uint256 pewpewReceived,
        uint256 pewpewBurned
    );

    event SeasonRaceLegResult(
        uint256 indexed seasonId,
        uint8 indexed legNumber,
        uint8 indexed trackId,
        address caller,
        uint256 callerRewardPewpew,
        uint256 winnerCarIdLeg,
        uint256 winnerTimeLeg
    );

    event SeasonFinalized(
        uint256 indexed seasonId,
        uint8 indexed trackIdFinal,
        uint256 winnerCarIdOverall,
        address winnerOwner,
        uint256 grandPrizePewswap5
    );

    event UpgradeEquipped(uint256 carId, uint8 slot, uint256 upgradeId);
    event UpgradeUnequipped(uint256 carId, uint8 slot, uint256 upgradeId);
    event UpgradeBurnedForLoss(address indexed player, uint256 carId, uint256 upgradeId);

    // -----------------------
    // Events (vault/rescue)
    // -----------------------
    // V2 events
    event DragRaceReplayStored(uint256 indexed raceId, uint256 winnerCarId, address winnerOwner, uint256 rewardPaid);
    event SingleRaceBatchStored(uint256 indexed raceId, uint8 indexed trackId, uint256 winnerCarId, address winnerOwner, uint256 winnerTime, uint256 rewardPaid);
    event RewardSkipped(address indexed to, uint256 amountRequested);
    event HolderVaultSend(address indexed caller, address indexed token, address indexed vault, uint256 amount);
    event Rescue(address indexed token, address indexed to, uint256 amount);

    // -----------------------
    // Visual manifests (read-only)
    // -----------------------
    struct SideViewVisuals {
        uint32 modelId;
        uint256 paintUpgradeId;
        uint256[] decalUpgradeIds;
        bool hasSupercharger;
    }

    struct TopViewVisuals {
        uint32 modelId;
        uint256 paintUpgradeId;
        uint256[] decalUpgradeIds;
        bool hasSupercharger;
    }

    struct PendingEquipSelection {
        uint8 slot;
        uint8 level;
        uint256 upgradeId;
    }

    // V2: external view helper contract (to keep GAME_CONTRACT deployable under code-size limit)
    // Expects functions:
    // - getSideViewVisuals(address game, uint256 carId) returns (uint32,uint256,uint256[],bool)
    // - getTopViewVisuals(address game, uint256 carId) returns (uint32,uint256,uint256[],bool)
    address internal views;

    // V2: aux helper contract for replay storage (can be the same address as `views`)
    address internal aux;

    // -----------------------
    // CONSTRUCTOR — NO INPUTS
    // -----------------------
    constructor() {
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);

        // Core system contracts (defaults, owner adjustable)
        carNFT = IRaceCarNFT(0x344222c442AC321887398419A6Be4e7F1dE56632);
        upgradeRegistry = IUpgradeRegistry(0xCC8764cDfc9318fB1b50b46B47f0f036c32a793C);
        upgrades1155 = IRacerUpgrades1155GameBurn(0xBdfd50B2Be6f1D578128905AABc657cDA3a8AD74);

        // Economic / routing contracts (defaults, owner adjustable)
        pewpew = IERC20(0x9deeaF046e144Fb6304A5ACD2aF142bBfE958030);
        usdc = IERC20(0x15D38573d2feeb82e7ad5187aB8c1D52810B1f07);
        router = IPulseXRouterV2(0x165C3410fC91EF562C50559f7d2289fEbed552d9);

        // Rewards token (PEWSWAP5)
        pewswap5 = IERC20(0xE318a5c4bFEBb23DA12A47ac429B8325B7D45B80);

        benevolenceVault = DEFAULT_BENEVOLENCE_VAULT;
        lpHarvesterVault = DEFAULT_FARMERS_LPUSDC_VAULT;

        // Default season tracks: 0..4
        seasonIncludedTracks[0] = 0;
        seasonIncludedTracks[1] = 1;
        seasonIncludedTracks[2] = 2;
        seasonIncludedTracks[3] = 3;
        seasonIncludedTracks[4] = 4;

        _initLaunchTracks();
    }

    function setGameView(address a) external onlyOwner {
        if (a == address(0)) revert ToZero();
        views = a;
        aux = a;
    }

    function _aux() internal view returns (address a) {
        a = aux;
        if (a == address(0)) revert HelperNotSet();
    }

    // -----------------------
    // Receive PLS
    // -----------------------
    receive() external payable {}

    // -----------------------
    // Track init (5 maps live)
    // Track IDs and defaults per your table:
    // 0 Oval Speedway (Speed, 7–9%)
    // 1 Wavy Speedway (Handling, 10–12%)
    // 2 Mountain Pass (Accel, 12–15%)
    // 3 Oceanfront Raceway (Speed+Handling, 8–10%)
    // 4 Rounded Triangle Circuit (Handling, 9–11%)
    // -----------------------
    function _initLaunchTracks() internal {
        // multipliers are mild; randomness range is stored in upsetChanceMin/Max.
        _addTrackInternal(1120, 1000, 980, 800);
        upsetChanceMinBps[0] = 700;
        upsetChanceMaxBps[0] = 900;

        _addTrackInternal(990, 1000, 1120, 1100);
        upsetChanceMinBps[1] = 1000;
        upsetChanceMaxBps[1] = 1200;

        _addTrackInternal(980, 1150, 1000, 1300);
        upsetChanceMinBps[2] = 1200;
        upsetChanceMaxBps[2] = 1500;

        _addTrackInternal(1080, 980, 1080, 900);
        upsetChanceMinBps[3] = 800;
        upsetChanceMaxBps[3] = 1000;

        _addTrackInternal(980, 990, 1140, 1000);
        upsetChanceMinBps[4] = 900;
        upsetChanceMaxBps[4] = 1100;
    }

    function _addTrackInternal(
        uint16 speedMult,
        uint16 accelMult,
        uint16 handlingMult,
        uint16
    ) internal {
        if (trackCount >= MAX_TRACKS) revert MaxTracks();
        tracks[trackCount] = Track(speedMult, accelMult, handlingMult);
        trackCount++;
    }

    // -----------------------
    // Track admin
    // -----------------------
    function ownerAddTrack(
        uint16 speedMult,
        uint16 accelMult,
        uint16 handlingMult,
        uint16 upsetBps_
    ) external onlyOwner {
        _addTrackInternal(speedMult, accelMult, handlingMult, upsetBps_);
        uint8 tid = trackCount - 1;
        upsetChanceMinBps[tid] = upsetBps_;
        upsetChanceMaxBps[tid] = upsetBps_;
    }

    function ownerSetTrack(
        uint8 trackId,
        uint16 speedMult,
        uint16 accelMult,
        uint16 handlingMult,
        uint16 upsetBps_
    ) external onlyOwner {
        if (trackId >= trackCount) revert InvalidTrack();
        tracks[trackId] = Track(speedMult, accelMult, handlingMult);
        upsetChanceMinBps[trackId] = upsetBps_;
        upsetChanceMaxBps[trackId] = upsetBps_;
    }

    function ownerSetTrackRandomnessRange(uint8 trackId, uint16 minBps, uint16 maxBps) external onlyOwner {
        if (trackId >= trackCount) revert InvalidTrack();
        if (maxBps < minBps) revert InvalidRandomnessRange();
        if (maxBps > MAX_BPS) revert InvalidRandomnessRange();
        upsetChanceMinBps[trackId] = minBps;
        upsetChanceMaxBps[trackId] = maxBps;
    }

    // -----------------------
    // OWNER SETTERS — FULL CONTROL
    // -----------------------
    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert NewOwnerZero();
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }

    function setCoreContracts(
        address _carNFT,
        address _upgradeRegistry,
        address _upgrades1155
    ) external onlyOwner {
        if (_carNFT == address(0) || _upgradeRegistry == address(0) || _upgrades1155 == address(0)) revert ToZero();
        carNFT = IRaceCarNFT(_carNFT);
        upgradeRegistry = IUpgradeRegistry(_upgradeRegistry);
        upgrades1155 = IRacerUpgrades1155GameBurn(_upgrades1155);
    }

    function setTokens(address _pewpew, address _usdc) external onlyOwner {
        if (_pewpew == address(0) || _usdc == address(0)) revert ToZero();
        pewpew = IERC20(_pewpew);
        usdc = IERC20(_usdc);
    }

    function setEconomyWiring(
        address _router,
        address _pewswap5
    ) external onlyOwner {
        if (_router == address(0) || _pewswap5 == address(0)) revert ToZero();
        router = IPulseXRouterV2(_router);
        pewswap5 = IERC20(_pewswap5);
    }

    function setBenandSetLPvault(address benevolence, address lpHarvester) external onlyOwner {
        if (benevolence == address(0) || lpHarvester == address(0)) revert ToZero();
        benevolenceVault = benevolence;
        lpHarvesterVault = lpHarvester;
    }

    function setVaultBlastConfig(
        uint256 minHold,
        uint256 amount,
        uint256[5] calldata thresholds,
        uint16[5] calldata bps
    ) external onlyOwner {
        minPewpewToVaultBlast = minHold;
        maxPewpewVaultSend = amount;
        for (uint256 i = 0; i < 5; i++) {
            if (i > 0 && thresholds[i] < thresholds[i - 1]) revert TooHigh();
            if (bps[i] > MAX_BPS) revert TooHigh();
            vaultBlastTierThresholds[i] = thresholds[i];
            vaultBlastTierRewardBps[i] = bps[i];
        }
    }

    function setRaceFees(
        uint256 _ghost,
        uint256 _drag,
        uint256 _single,
        uint256 _season
    ) external onlyOwner {
        ghostRaceFee = _ghost;
        dragRaceFee = _drag;
        singleRaceFee = _single;
        seasonStartFee = _season;
    }

    function setSlippageBps(uint16 bps) external onlyOwner {
        if (bps > 2000) revert TooHigh();
        slippageBps = bps;
    }

    function setStarterWinRewardBps(uint16 _dragBps, uint16 _singleBps) external onlyOwner {
        if (_dragBps > MAX_BPS) revert TooHigh();
        if (_singleBps > MAX_BPS) revert TooHigh();
        dragStarterWinRewardBps = _dragBps;
        singleStarterWinRewardBps = _singleBps;
    }

    function setSeasonEconomy(uint16 _burnBps, uint16 _championRewardBps) external onlyOwner {
        if (_burnBps > MAX_BPS) revert TooHigh();
        if (_championRewardBps > MAX_BPS) revert TooHigh();
        seasonBurnBps = _burnBps;
        seasonChampionRewardBps = _championRewardBps;
    }

    function setSeasonBlastTierRewardBps(uint16[5] calldata bps) external onlyOwner {
        for (uint256 i = 0; i < 5; i++) {
            if (bps[i] > MAX_BPS) revert TooHigh();
            seasonBlastTierRewardBps[i] = bps[i];
        }
    }

    function setMinPewpewToSeasonBlast(uint256 amount) external onlyOwner {
        minPewpewToSeasonBlast = amount;
    }

    function setSeasonLegCooldownSeconds(uint32 s) external onlyOwner {
        seasonLegCooldownSeconds = s;
    }

    function setSeasonIncludedTracks(uint8[SEASON_LEGS] calldata ids) external onlyOwner {
        for (uint8 i = 0; i < SEASON_LEGS; i++) {
            if (ids[i] >= trackCount) revert InvalidTrack();
            seasonIncludedTracks[i] = ids[i];
        }
    }

    function setSeasonHistoryLimit(uint256 n) external onlyOwner {
        seasonHistoryLimit = n;
    }

    function setPaused(bool p) external onlyOwner {
        paused = p;
        emit PausedSet(p);
    }

    // -----------------------
    // Unified rescue (ERC20 or native PLS)
    // -----------------------
    function _safeTransfer(address token, address to, uint256 amount) internal returns (bool ok) {
        // IERC20.transfer selector = 0xa9059cbb
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, amount));
        if (!success) return false;
        if (data.length == 0) return true; // non-standard ERC20
        if (data.length >= 32) return abi.decode(data, (bool));
        return false;
    }

    function _rescue(address token, address payable to, uint256 amount) internal {
        if (to == address(0)) revert ToZero();
        if (token == address(0)) {
            (bool ok, ) = to.call{value: amount}("");
            if (!ok) revert PlsSendFailed();
        } else {
            if (!_safeTransfer(token, to, amount)) revert TransferFailed();
        }
        emit Rescue(token, to, amount);
    }

    function rescue(address token, address payable to, uint256 amount) external onlyOwner nonReentrant {
        _rescue(token, to, amount);
    }

    function rescuePLS(address payable to, uint256 amount) external onlyOwner nonReentrant {
        _rescue(address(0), to, amount);
    }

    // -----------------------
    // Balance getters
    // -----------------------
    function getTokenBalances() external view returns (uint256 usdcBal, uint256 pewpewBal, uint256 pewswap5Bal) {
        return (usdc.balanceOf(address(this)), pewpew.balanceOf(address(this)), pewswap5.balanceOf(address(this)));
    }

    // -----------------------
    // Read-only helpers (front end)
    // -----------------------
    function getWiredAddresses()
        external
        view
        returns (
            address carNft,
            address registry,
            address upgrades,
            address pewpewToken,
            address usdcToken,
            address pewswap5Token,
            address routerAddr,
            address viewsAddr,
            address auxAddr,
            address benevolence,
            address lpHarvester
        )
    {
        return (
            address(carNFT),
            address(upgradeRegistry),
            address(upgrades1155),
            address(pewpew),
            address(usdc),
            address(pewswap5),
            address(router),
            views,
            aux,
            benevolenceVault,
            lpHarvesterVault
        );
    }

    function getInternalConfig()
        external
        view
        returns (
            uint16 _slippageBps,
            uint32 _seasonLegCooldownSeconds,
            uint256 _seasonHistoryLimit
        )
    {
        return (slippageBps, seasonLegCooldownSeconds, seasonHistoryLimit);
    }

    function getSeasonIncludedTracks() external view returns (uint8[SEASON_LEGS] memory ids) {
        return seasonIncludedTracks;
    }

    function getSeasonLegCooldownRemaining(uint256 seasonId) external view returns (uint256 remainingSeconds) {
        uint256 readyAt = seasonNextLegAvailableAt[seasonId];
        return readyAt == 0 || block.timestamp >= readyAt ? 0 : readyAt - block.timestamp;
    }

    function getVaultBlastPolicy()
        external
        view
        returns (
            uint256[5] memory thresholds,
            uint16[5] memory rewardBps,
            uint256 pewpewMaxSend
        )
    {
        thresholds = vaultBlastTierThresholds;
        rewardBps = vaultBlastTierRewardBps;
        pewpewMaxSend = maxPewpewVaultSend;
    }

    // -----------------------
    // V2: PEWPEW-holder can send USDC/PEWPEW from this contract to vaults (capped)
    // -----------------------
    function holderSendToVault(address token, uint256 amount, bool toLpVault) external nonReentrant whenNotPaused {
        uint256 callerBal = pewpew.balanceOf(msg.sender);
        // Vault Blast gating is separate from season-blast tiers.
        if (callerBal < minPewpewToVaultBlast) revert NeedPewpew();

        address vault = toLpVault ? lpHarvesterVault : benevolenceVault;
        if (vault == address(0)) revert VaultZero();

        // Tier index (0..4) for 1m/5m/10m/20m/30m.
        uint8 tier;
        if (callerBal >= vaultBlastTierThresholds[4]) tier = 4;
        else if (callerBal >= vaultBlastTierThresholds[3]) tier = 3;
        else if (callerBal >= vaultBlastTierThresholds[2]) tier = 2;
        else if (callerBal >= vaultBlastTierThresholds[1]) tier = 1;
        else tier = 0;

        // USDC send: capped to 5 USDC per call; pays the same tiered % reward system as PEWPEW.
        if (token == address(usdc)) {
            if (amount > 5_000_000) revert TooHigh(); // 5 USDC (6 decimals)

            uint256 balBefore = IERC20(token).balanceOf(address(this));
            if (!_safeTransfer(token, vault, amount)) revert TransferFailed();

            uint256 usdcReward = (amount * uint256(vaultBlastTierRewardBps[tier])) / MAX_BPS;
            uint256 rem = balBefore - amount;
            if (usdcReward > rem) usdcReward = rem;
            if (usdcReward > 0 && !_safeTransfer(token, msg.sender, usdcReward)) revert TransferFailed();

            emit HolderVaultSend(msg.sender, token, vault, amount);
            return;
        }

        // PEWPEW send: LP vault only, capped to 5,000 PEWPEW per call; pays tiered PEWPEW % reward.
        if (token == address(pewpew)) {
            if (!toLpVault) revert TransferFailed();
            if (amount > maxPewpewVaultSend) revert TooHigh();

            uint256 balBefore = pewpew.balanceOf(address(this));

            if (!_safeTransfer(token, vault, amount)) revert TransferFailed();

            uint256 reward = (amount * uint256(vaultBlastTierRewardBps[tier])) / MAX_BPS;
            uint256 rem = balBefore - amount;
            if (reward > rem) reward = rem;
            if (reward > 0 && !_safeTransfer(token, msg.sender, reward)) revert TransferFailed();

            emit HolderVaultSend(msg.sender, token, vault, amount);
            return;
        }

        revert TransferFailed();
    }

    // -----------------------
    // EQUIP / UNEQUIP LOGIC
    // -----------------------
    function equipUpgrade(uint256 carId, uint256[] calldata upgradeIds) external nonReentrant {
        if (carNFT.ownerOf(carId) != msg.sender) revert NotCarOwner();

        uint256 len = upgradeIds.length;
        PendingEquipSelection[] memory selected = new PendingEquipSelection[](len);
        uint256 selectedCount = 0;

        for (uint256 i = 0; i < len; i++) {
            uint256 upgradeId = upgradeIds[i];
            if (!upgradeRegistry.exists(upgradeId)) revert InvalidUpgrade();
            if (upgrades1155.balanceOf(msg.sender, upgradeId) < 1) revert NoToken();

            uint8 slot = upgradeRegistry.getSlotType(upgradeId);
            if (slot >= MAX_SLOTS) revert SlotTooLarge();

            uint8 level = upgradeRegistry.getLevel(upgradeId);
            bool handled = false;

            for (uint256 j = 0; j < selectedCount; j++) {
                if (selected[j].slot != slot) continue;
                if (level > selected[j].level) {
                    selected[j].level = level;
                    selected[j].upgradeId = upgradeId;
                }
                handled = true;
                break;
            }

            if (!handled) {
                selected[selectedCount] = PendingEquipSelection({
                    slot: slot,
                    level: level,
                    upgradeId: upgradeId
                });
                selectedCount++;
            }
        }

        for (uint256 i = 0; i < selectedCount; i++) {
            uint8 slot = selected[i].slot;
            uint256 upgradeId = selected[i].upgradeId;
            equippedUpgradeByCarSlot[carId][slot] = upgradeId;
            emit UpgradeEquipped(carId, slot, upgradeId);
        }
    }

    function unequipUpgrade(uint256 carId, uint8[] calldata slots) external nonReentrant {
        if (carNFT.ownerOf(carId) != msg.sender) revert NotCarOwner();

        uint256 len = slots.length;
        for (uint256 i = 0; i < len; i++) {
            uint8 slot = slots[i];
            if (slot >= MAX_SLOTS) revert SlotTooLarge();

            bool seen = false;
            for (uint256 j = 0; j < i; j++) {
                if (slots[j] == slot) {
                    seen = true;
                    break;
                }
            }
            if (seen) continue;

            uint256 current = equippedUpgradeByCarSlot[carId][slot];
            if (current == 0) revert NothingEquipped();
            equippedUpgradeByCarSlot[carId][slot] = 0;
            emit UpgradeUnequipped(carId, slot, current);
        }
    }

    // -----------------------
    // EFFECTIVE STATS
    // -----------------------
    function getCarEffectiveStats(uint256 carId)
        public
        view
        returns (uint16 effSpeed, uint16 effAccel, uint16 effHandling)
    {
        (, , uint16 baseSpeed, uint16 baseHandling, uint16 baseAccel) = carNFT.cars(carId);

        int256 speed = int256(uint256(baseSpeed));
        int256 accel = int256(uint256(baseAccel));
        int256 handling = int256(uint256(baseHandling));

        uint8 slots = upgradeRegistry.slotCount();
        if (slots > MAX_SLOTS) slots = MAX_SLOTS;

        address ownerOfCar = carNFT.ownerOf(carId);

        for (uint8 s = 0; s < slots; s++) {
            uint256 upId = equippedUpgradeByCarSlot[carId][s];
            if (upId == 0) continue;
            if (upgrades1155.balanceOf(ownerOfCar, upId) == 0) continue;

            (int16 ds, int16 da, int16 dh) = upgradeRegistry.getStats(upId);
            speed += ds;
            accel += da;
            handling += dh;
        }

        effSpeed = _clampToUint16(speed);
        effAccel = _clampToUint16(accel);
        effHandling = _clampToUint16(handling);
    }

    function _clampToUint16(int256 v) internal pure returns (uint16) {
        if (v < 0) return 0;
        if (v > int256(uint256(type(uint16).max))) return type(uint16).max;
        return uint16(uint256(v));
    }

    // -----------------------
    // INTERNAL: PLS -> token swaps (via aux helper)
    // -----------------------
    function _swapPlsForMode(uint8 mode, uint256 plsAmount) internal returns (uint256 outReceived) {
        if (plsAmount == 0) return 0;
        outReceived = IGAME_AUX(_aux()).swapExactPlsForMode{value: plsAmount}(mode, address(router), slippageBps);
    }

    // V2: non-reverting payout for gameplay (skip if insufficient)
    function _tryPayRewardPewswap5(address to, uint256 amount) internal returns (uint256 paid) {
        if (amount == 0) return 0;
        if (to == address(0)) revert ToZero();
        uint256 bal = pewswap5.balanceOf(address(this));
        if (bal < amount) {
            emit RewardSkipped(to, amount);
            return 0;
        }
        if (!_safeTransfer(address(pewswap5), to, amount)) revert RewardTransferFailed();
        return amount;
    }

    // -----------------------
    // INTERNAL: RANDOM EQUIPPED TOKEN BURN (upgrade OR visual)
    // -----------------------
    function _burnRandomHighestUpgrade(address player, uint256 carId) internal {
        uint256 chosenId = IGAME_AUX(_aux()).pickRandomEquippedToken(player, carId);

        if (chosenId != 0) {
            upgrades1155.burnForGame(player, chosenId, 1);
            emit UpgradeBurnedForLoss(player, carId, chosenId);
        }
    }

    // -----------------------
    // INTERNAL: DRAG TIME (NO MODIFIERS)
    // -----------------------
    function _computeDragTime(uint256 carId) internal view returns (uint256) {
        (uint16 s, uint16 a, uint16 h) = getCarEffectiveStats(carId);

        uint256 time = 200_000 - (uint256(s) * 40 + uint256(a) * 30 + uint256(h) * 10) / 100;
        if (time < 10_000) time = 10_000;
        return time;
    }

    // -----------------------
    // INTERNAL: TRACK TIME (WITH MODIFIERS + UPSET)
    // -----------------------
    function _computeBaseTrackTime(uint256 carId, uint8 trackId)
        internal
        view
        returns (uint256 baseTime, uint16 minBps, uint16 maxBps)
    {
        Track storage tr = tracks[trackId];
        (uint16 s, uint16 a, uint16 h) = getCarEffectiveStats(carId);

        uint256 adjS = (uint256(s) * tr.speedMult) / 1000;
        uint256 adjA = (uint256(a) * tr.accelMult) / 1000;
        uint256 adjH = (uint256(h) * tr.handlingMult) / 1000;

        baseTime = 250_000 - (adjS * 30 + adjA * 40 + adjH * 30) / 100;
        if (baseTime < 15_000) baseTime = 15_000;

        minBps = upsetChanceMinBps[trackId];
        maxBps = upsetChanceMaxBps[trackId];
    }

    function _applyUpset(uint256 baseTime, uint16 minBps, uint16 maxBps, uint256 seed)
        internal
        pure
        returns (uint256)
    {
        if (maxBps == 0) return baseTime;

        uint16 chance;
        if (maxBps == minBps) {
            chance = minBps;
        } else {
            chance = uint16(minBps + (seed % (uint256(maxBps - minBps) + 1)));
        }

        uint256 roll = uint256(keccak256(abi.encodePacked(seed, uint256(1)))) % 10_000;
        if (roll < chance) {
            return (baseTime * 110) / 100;
        }
        return baseTime;
    }

    function _computeTrackTimeFromEntropy(uint256 carId, uint8 trackId, bytes32 entropyRoot)
        internal
        view
        returns (uint256)
    {
        if (trackId >= trackCount) revert InvalidTrack();

        (uint256 baseTime, uint16 minBps, uint16 maxBps) = _computeBaseTrackTime(carId, trackId);
        if (maxBps == 0) return baseTime;

        uint256 seed = uint256(
            keccak256(abi.encodePacked(entropyRoot, carId, trackId, baseTime))
        );
        return _applyUpset(baseTime, minBps, maxBps, seed);
    }

    function _computeTrackTime(uint256 carId, uint8 trackId) internal view returns (uint256) {
        return _computeTrackTimeFromEntropy(carId, trackId, blockhash(block.number - 1));
    }

    function _computeSeasonTrackTime(
        uint256 carId,
        uint8 trackId,
        bytes32 entropyRoot
    ) internal view returns (uint256) {
        return _computeTrackTimeFromEntropy(carId, trackId, entropyRoot);
    }

    function _nextSeasonEntropy(uint256 seasonId, uint8 leg, address raceCaller)
        internal
        returns (bytes32 entropyRoot)
    {
        uint256 raceNonce = seasonRaceNonce[seasonId] + 1;
        seasonRaceNonce[seasonId] = raceNonce;

        bytes32 delayedBlockHash = block.number > 5 ? blockhash(block.number - 5) : blockhash(block.number - 1);
        entropyRoot = keccak256(
            abi.encodePacked(
                seasonEntropyAccumulator,
                block.prevrandao,
                delayedBlockHash,
                seasonId,
                leg,
                raceCaller,
                raceNonce
            )
        );

        seasonEntropyAccumulator = uint256(entropyRoot);
    }

    // -----------------------
    // DRAG RACE (2 CARS)
    // Winner always declared; reward (if configured) goes to the winner owner.
    // Existing burn-on-loss for starter remains.
    // -----------------------
    function dragRace(uint256 car1, uint256 car2) external payable nonReentrant whenNotPaused {
        if (car1 == car2) revert SameCar();
        if (carNFT.ownerOf(car1) != msg.sender) revert StarterMustOwnCar1();

        if (msg.value != dragRaceFee) revert WrongPlsAmount();
        uint256 pewswap5Received = _swapPlsForMode(2, dragRaceFee);

        uint256 t1 = _computeDragTime(car1);
        uint256 t2 = _computeDragTime(car2);

        if (bestDragTimeMsByCar[car1] == 0 || t1 < bestDragTimeMsByCar[car1]) {
            bestDragTimeMsByCar[car1] = t1;
        }
        if (bestDragTimeMsByCar[car2] == 0 || t2 < bestDragTimeMsByCar[car2]) {
            bestDragTimeMsByCar[car2] = t2;
        }

        bool starterWon = t1 <= t2;
        uint256 winnerCarId = starterWon ? car1 : car2;
        address winnerOwner = carNFT.ownerOf(winnerCarId);

        if (!starterWon) {
            _burnRandomHighestUpgrade(msg.sender, car1);
        }

        uint256 rewardPaid = 0;
        if (starterWon) {
            uint256 payout = (pewswap5Received * uint256(dragStarterWinRewardBps)) / MAX_BPS;
            rewardPaid = _tryPayRewardPewswap5(msg.sender, payout);
        }

        emit DragRaceResult(car1, t1, car2, t2, msg.sender, starterWon);

        uint256 rid = nextDragRaceId++;
        IGAME_AUX(_aux()).storeDragRaceReplay(
            rid,
            car1,
            car2,
            t1,
            t2,
            winnerCarId,
            winnerOwner,
            msg.sender,
            uint64(block.timestamp),
            rewardPaid
        );
        emit DragRaceReplayStored(rid, winnerCarId, winnerOwner, rewardPaid);
    }

    // -----------------------
    // GHOST DRAG RACE (FREE)
    // -----------------------
    function ghostDragRace(uint256 carId) external payable nonReentrant whenNotPaused {
        if (carNFT.ownerOf(carId) != msg.sender) revert NotCarOwner();

        if (msg.value != ghostRaceFee) revert WrongPlsAmount();
        _swapPlsForMode(1, ghostRaceFee);

        uint256 newTime = _computeDragTime(carId);
        uint256 prevBest = bestDragTimeMsByCar[carId];

        if (prevBest == 0 || newTime < prevBest) {
            bestDragTimeMsByCar[carId] = newTime;
        }

        emit GhostRaceResult(carId, newTime, prevBest);
    }

    // -----------------------
    // SINGLE RACE (V2, up to 32 cars)
    // - starter must own carIds[0] to prevent arbitrary third-party race submissions
    // - winner is lowest computed time
    // - reward (if any) goes to winner owner
    // - results stored for replay
    // -----------------------
    function singleRaceBatch(uint256[] calldata carIds, uint8 trackId)
        external
        payable
        nonReentrant
        whenNotPaused
        returns (uint256 raceId, uint256 winnerCarId, uint256 winnerTime)
    {
        if (carIds.length < 2) revert Need2Cars();
        if (carIds.length > MAX_SINGLE_RACE_CARS) revert TooManyCars();
        if (carNFT.ownerOf(carIds[0]) != msg.sender) revert StarterMustOwnFirstCar();
        if (trackId >= trackCount) revert InvalidTrack();

        IGAME_AUX(_aux()).validateNoZeroNoDups(carIds);

        uint256[] memory cars = new uint256[](carIds.length);
        for (uint256 i = 0; i < carIds.length; i++) {
            cars[i] = carIds[i];
        }

        (raceId, winnerCarId, winnerTime,,) = _singleRaceBatch(cars, trackId);
    }

    function _singleRaceBatch(uint256[] memory carIds, uint8 trackId)
        internal
        returns (uint256 raceId, uint256 winnerCarId, uint256 winnerTime, address winnerOwner, uint256 rewardPaid)
    {
        if (msg.value != singleRaceFee) revert WrongPlsAmount();
        uint256 pewswap5Received = _swapPlsForMode(3, singleRaceFee);

        raceId = nextSingleRaceBatchId++;

        winnerTime = type(uint256).max;
        winnerCarId = 0;

        uint256[] memory times = new uint256[](carIds.length);

        for (uint256 i2 = 0; i2 < carIds.length; i2++) {
            uint256 cid = carIds[i2];
            uint256 t = _computeTrackTime(cid, trackId);

            uint256 prev = bestTrackTimeMsByCarTrack[cid][trackId];
            if (prev == 0 || t < prev) {
                bestTrackTimeMsByCarTrack[cid][trackId] = t;
            }

            times[i2] = t;

            if (t < winnerTime) {
                winnerTime = t;
                winnerCarId = cid;
            }
        }

        winnerOwner = carNFT.ownerOf(winnerCarId);

        // Burn 1 upgrade if the starter (carIds[0]) loses.
        if (carIds[0] != winnerCarId) {
            _burnRandomHighestUpgrade(msg.sender, carIds[0]);
            rewardPaid = 0;
        } else {
            uint256 payout = (pewswap5Received * uint256(singleStarterWinRewardBps)) / MAX_BPS;
            rewardPaid = _tryPayRewardPewswap5(msg.sender, payout);
        }
        // No payout to non-starter winners.

        IGAME_AUX(_aux()).storeSingleRaceBatchReplay(
            raceId,
            trackId,
            carIds,
            times,
            winnerCarId,
            winnerOwner,
            winnerTime,
            msg.sender,
            uint64(block.timestamp),
            rewardPaid
        );

        emit SingleRaceBatchStored(raceId, trackId, winnerCarId, winnerOwner, winnerTime, rewardPaid);
    }

    // -----------------------
    // SEASONS (AGGREGATE, 5 LEGS, FIXED ROSTER)
    // -----------------------
    function getSeasonCars(uint256 seasonId) external view returns (uint256[] memory) {
        return _seasonCars[seasonId];
    }

    function startSeason(uint256 starterCarId, uint256[] calldata carIds)
        external
        payable
        nonReentrant
        whenNotPaused
        returns (uint256 seasonId)
    {
        if (activeSeasons.length >= 5) revert SeasonLimit();
        if (carIds.length < 2) revert Need2Cars();
        if (carIds.length > MAX_RACE_CARS) revert TooManyCars();
        if (carIds[0] != starterCarId) revert StarterCarMismatch();
        if (carNFT.ownerOf(starterCarId) != msg.sender) revert NotCarOwner();
        if (msg.value != seasonStartFee) revert WrongPlsAmount();

        IGAME_AUX(_aux()).validateNoZeroNoDups(carIds);

        uint256 pewpewReceived = _swapPlsForMode(4, seasonStartFee);
        uint256 burnAmount = (pewpewReceived * uint256(seasonBurnBps)) / MAX_BPS;
        if (burnAmount > pewpewReceived) revert BurnGreaterThanReceived();
        if (burnAmount > 0) IERC20Burnable(address(pewpew)).burn(burnAmount);

        seasonId = nextSeasonId++;
        activeSeasons.push(seasonId);
        isSeasonActive[seasonId] = true;

        seasonRaceCount[seasonId] = 0;
        seasonNextLegAvailableAt[seasonId] = 0;

        for (uint256 k = 0; k < carIds.length; k++) {
            _seasonCars[seasonId].push(carIds[k]);
        }

        emit SeasonStarted(
            seasonId,
            msg.sender,
            starterCarId,
            carIds.length,
            pewpewReceived,
            burnAmount
        );
    }

    function endSeason(uint256 seasonId) external onlyOwner {
        if (!isSeasonActive[seasonId]) revert NotActive();
        _endSeasonInternal(seasonId);
        _pushSeasonHistoryAndMaybePurge(seasonId);
    }

    function _endSeasonInternal(uint256 seasonId) internal {
        isSeasonActive[seasonId] = false;

        uint256 len = activeSeasons.length;
        for (uint256 i = 0; i < len; i++) {
            if (activeSeasons[i] == seasonId) {
                activeSeasons[i] = activeSeasons[len - 1];
                activeSeasons.pop();
                break;
            }
        }
    }

    // Leg 1: run by anyone
    function seasonRace(uint256 seasonId) external nonReentrant whenNotPaused {
        if (!isSeasonActive[seasonId]) revert SeasonInactive();

        uint8 leg = seasonRaceCount[seasonId] + 1;
        if (leg != 1) revert SeasonLegCooldown();

        _runSeasonLeg(seasonId, leg, false);
    }

    // Legs 2..5: run via blast; caller must hold enough PEWPEW and respects cooldown
    function blastSeasonRace(uint256 seasonId) external nonReentrant whenNotPaused {
        if (!isSeasonActive[seasonId]) revert SeasonInactive();
        if (pewpew.balanceOf(msg.sender) < minPewpewToSeasonBlast) revert Need1MPewpew();

        uint8 leg = seasonRaceCount[seasonId] + 1;
        if (leg < 2 || leg > SEASON_LEGS) revert SeasonComplete();

        uint256 readyAt = seasonNextLegAvailableAt[seasonId];
        if (readyAt != 0 && block.timestamp < readyAt) revert SeasonLegCooldown();

        _runSeasonLeg(seasonId, leg, true);
    }

    function _runSeasonLeg(
        uint256 seasonId,
        uint8 leg,
        bool payBlastReward
    ) internal {
        uint8 trackId = seasonIncludedTracks[leg - 1];
        if (trackId >= trackCount) revert InvalidTrack();

        bytes32 entropyRoot = _nextSeasonEntropy(seasonId, leg, msg.sender);

        (uint256 bestLegTime, uint256 winnerLegCarId) = _seasonLegComputeAndUpdate(
            seasonId,
            trackId,
            entropyRoot
        );

        uint256 callerReward = 0;
        if (payBlastReward) {
            uint16 tierBps;
            uint256 callerBal = pewpew.balanceOf(msg.sender);
            if (callerBal >= SEASON_BLAST_TIER5) tierBps = seasonBlastTierRewardBps[4];
            else if (callerBal >= SEASON_BLAST_TIER4) tierBps = seasonBlastTierRewardBps[3];
            else if (callerBal >= SEASON_BLAST_TIER3) tierBps = seasonBlastTierRewardBps[2];
            else if (callerBal >= SEASON_BLAST_TIER2) tierBps = seasonBlastTierRewardBps[1];
            else tierBps = seasonBlastTierRewardBps[0];

            callerReward = (pewpew.balanceOf(address(this)) * uint256(tierBps)) / MAX_BPS;
            if (callerReward > 0) {
                uint256 bal = pewpew.balanceOf(address(this));
                if (callerReward > bal) callerReward = bal;
                if (callerReward > 0) {
                    if (!_safeTransfer(address(pewpew), msg.sender, callerReward)) revert CallerPewpewTransferFailed();
                }
            }
        }

        seasonRaceCount[seasonId] = leg;

        IGAME_AUX(_aux()).storeSeasonLeg(seasonId, leg, trackId, winnerLegCarId, bestLegTime, block.timestamp);

        emit SeasonRaceLegResult(seasonId, leg, trackId, msg.sender, callerReward, winnerLegCarId, bestLegTime);

        if (leg < SEASON_LEGS) {
            seasonNextLegAvailableAt[seasonId] = block.timestamp + seasonLegCooldownSeconds;
        }

        if (leg == SEASON_LEGS) {
            (uint256 winnerOverallCarId, address winnerOwner, uint256 prizePaid) = _finalizeSeasonAndTryPay(seasonId);
            emit SeasonFinalized(seasonId, trackId, winnerOverallCarId, winnerOwner, prizePaid);
            _endSeasonInternal(seasonId);
            _pushSeasonHistoryAndMaybePurge(seasonId);
        }
    }

    function _seasonLegComputeAndUpdate(
        uint256 seasonId,
        uint8 trackId,
        bytes32 entropyRoot
    ) internal returns (uint256 bestLegTime, uint256 winnerLegCarId) {
        uint256[] storage carIds = _seasonCars[seasonId];
        if (carIds.length < 2) revert NoRoster();

        bestLegTime = type(uint256).max;

        for (uint256 i = 0; i < carIds.length; i++) {
            uint256 cid = carIds[i];
            uint256 t = _computeSeasonTrackTime(cid, trackId, entropyRoot);

            uint256 bestGlobal = bestTrackTimeMsByCarTrack[cid][trackId];
            if (bestGlobal == 0 || t < bestGlobal) {
                bestTrackTimeMsByCarTrack[cid][trackId] = t;
            }

            seasonTotalTimeMs[seasonId][cid] += t;

            if (t < bestLegTime) {
                bestLegTime = t;
                winnerLegCarId = cid;
            }
        }
    }

    function _finalizeSeasonAndTryPay(uint256 seasonId)
        internal
        returns (uint256 winnerCarIdOverall, address winnerOwner, uint256 prizePaid)
    {
        uint256[] storage roster = _seasonCars[seasonId];

        uint256 n = roster.length;
        uint256 bestTotal = type(uint256).max;
        uint256 bestCar = 0;
        address bestOwner = address(0);

        for (uint256 i = 0; i < n; i++) {
            uint256 cid = roster[i];
            address o = carNFT.ownerOf(cid);
            uint256 total = seasonTotalTimeMs[seasonId][cid];
            if (total < bestTotal) {
                bestTotal = total;
                bestCar = cid;
                bestOwner = o;
            }
        }

        winnerCarIdOverall = bestCar;
        winnerOwner = bestOwner;

        prizePaid = 0;
        if (winnerCarIdOverall != 0) {
            uint256 payout = (pewswap5.balanceOf(address(this)) * uint256(seasonChampionRewardBps)) / MAX_BPS;
            prizePaid = _tryPayRewardPewswap5(winnerOwner, payout);
        }

        IGAME_AUX(_aux()).storeSeasonFinal(seasonId, winnerCarIdOverall, winnerOwner, prizePaid);

        // No burn-on-loss for season starter (only drag/single modes burn).
    }

    function _pushSeasonHistoryAndMaybePurge(uint256 seasonId) internal {
        seasonHistory.push(seasonId);
        uint256 lim = seasonHistoryLimit;
        if (lim == 0) return;
        if (seasonHistory.length <= lim) return;

        uint256 oldId = seasonHistory[0];
        // shift left (small array; lim default 30)
        for (uint256 i = 0; i + 1 < seasonHistory.length; i++) {
            seasonHistory[i] = seasonHistory[i + 1];
        }
        seasonHistory.pop();

        _purgeSeasonData(oldId);
    }

    function _purgeSeasonData(uint256 seasonId) internal {
        uint256[] storage roster = _seasonCars[seasonId];
        uint256 n = roster.length;
        for (uint256 i = 0; i < n; i++) {
            uint256 cid = roster[i];
            delete seasonTotalTimeMs[seasonId][cid];
        }

        // clear roster
        delete _seasonCars[seasonId];

        delete seasonRaceCount[seasonId];
        delete seasonNextLegAvailableAt[seasonId];
        delete seasonRaceNonce[seasonId];

        IGAME_AUX(_aux()).purgeSeason(seasonId);
    }

    // -----------------------
    // Visual manifests (SIDE/TOP)
    // Implemented via an external views helper to keep this contract under the 24KB limit.
    // -----------------------
    function _getViewVisuals(bytes4 selector, uint256 carId)
        internal
        view
        returns (uint32 modelId, uint256 paintUpgradeId, uint256[] memory decalUpgradeIds, bool hasSupercharger)
    {
        address vc = views;
        if (vc == address(0)) revert HelperNotSet();

        (bool ok, bytes memory data) = vc.staticcall(
            abi.encodeWithSelector(selector, address(this), carId)
        );
        if (!ok) revert ViewsCallFailed();
        return abi.decode(data, (uint32, uint256, uint256[], bool));
    }

    function getSideViewVisuals(uint256 carId) external view returns (SideViewVisuals memory v) {
        (uint32 modelId, uint256 paintUpgradeId, uint256[] memory decalUpgradeIds, bool hasSupercharger) =
            _getViewVisuals(_SEL_GET_SIDE_VISUALS, carId);
        v.modelId = modelId;
        v.paintUpgradeId = paintUpgradeId;
        v.decalUpgradeIds = decalUpgradeIds;
        v.hasSupercharger = hasSupercharger;
    }

    function getTopViewVisuals(uint256 carId) external view returns (TopViewVisuals memory v) {
        (uint32 modelId, uint256 paintUpgradeId, uint256[] memory decalUpgradeIds, bool hasSupercharger) =
            _getViewVisuals(_SEL_GET_TOP_VISUALS, carId);
        v.modelId = modelId;
        v.paintUpgradeId = paintUpgradeId;
        v.decalUpgradeIds = decalUpgradeIds;
        v.hasSupercharger = hasSupercharger;
    }
}