1040 lines
48 KiB
PHP
1040 lines
48 KiB
PHP
<?php
|
|
/**
|
|
* Spacetraders mining fleet control page.
|
|
*
|
|
* @package SpacetradersAPI
|
|
* @author Keith Solomon <keith@keithsolomon.net>
|
|
* @license MIT License
|
|
* @version GIT: <git_id>
|
|
* @link https://git.keithsolomon.net/keith/Spacetraders
|
|
*/
|
|
|
|
require_once __DIR__ . '/lib/spacetraders-api.php';
|
|
require_once __DIR__ . '/lib/spacetraders-storage.php';
|
|
|
|
/**
|
|
* Filter only mining ships.
|
|
*
|
|
* @param array<int,array<string,mixed>> $ships
|
|
*
|
|
* @return array<int,array<string,mixed>>
|
|
*/
|
|
function filterMiningShips( array $ships ): array {
|
|
return array_values(
|
|
array_filter(
|
|
$ships,
|
|
static function ( array $ship ): bool {
|
|
$role = strtoupper( (string) ( $ship['registration']['role'] ?? '' ) );
|
|
return $role === 'EXCAVATOR';
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build contract delivery targets for outstanding delivery requirements.
|
|
*
|
|
* @param array<int,array<string,mixed>> $activeContracts
|
|
*
|
|
* @return array<int,array<string,mixed>>
|
|
*/
|
|
function buildContractDeliveryTargets( array $activeContracts ): array {
|
|
$targetsByKey = array();
|
|
|
|
foreach ( $activeContracts as $contract ) {
|
|
$contractId = (string) ( $contract['id'] ?? '' );
|
|
$deliveries = (array) ( $contract['terms']['deliver'] ?? array() );
|
|
|
|
if ($contractId === '' || empty( $deliveries ) ) {
|
|
continue;
|
|
}
|
|
|
|
foreach ( $deliveries as $delivery ) {
|
|
$tradeSymbol = (string) ( $delivery['tradeSymbol'] ?? '' );
|
|
$destinationSymbol = (string) ( $delivery['destinationSymbol'] ?? '' );
|
|
$unitsRequired = (int) ( $delivery['unitsRequired'] ?? 0 );
|
|
$unitsFulfilled = (int) ( $delivery['unitsFulfilled'] ?? 0 );
|
|
$remainingUnits = max( 0, $unitsRequired - $unitsFulfilled );
|
|
|
|
if ($tradeSymbol === '' || $destinationSymbol === '' || $remainingUnits <= 0 ) {
|
|
continue;
|
|
}
|
|
|
|
$key = $contractId . '|' . $destinationSymbol . '|' . $tradeSymbol;
|
|
|
|
if (! isset( $targetsByKey[ $key ] ) ) {
|
|
$targetsByKey[ $key ] = array(
|
|
'key' => $key,
|
|
'contractId' => $contractId,
|
|
'destinationSymbol' => $destinationSymbol,
|
|
'tradeSymbol' => $tradeSymbol,
|
|
'remainingUnits' => 0,
|
|
);
|
|
}
|
|
|
|
$targetsByKey[ $key ]['remainingUnits'] += $remainingUnits;
|
|
}
|
|
}
|
|
|
|
$targets = array_values( $targetsByKey );
|
|
|
|
usort(
|
|
$targets,
|
|
static function ( array $left, array $right ): int {
|
|
$leftSort = (string) ( $left['destinationSymbol'] ?? '' ) . '|' .
|
|
(string) ( $left['tradeSymbol'] ?? '' ) . '|' .
|
|
(string) ( $left['contractId'] ?? '' );
|
|
$rightSort = (string) ( $right['destinationSymbol'] ?? '' ) . '|' .
|
|
(string) ( $right['tradeSymbol'] ?? '' ) . '|' .
|
|
(string) ( $right['contractId'] ?? '' );
|
|
|
|
return strcmp( $leftSort, $rightSort );
|
|
}
|
|
);
|
|
|
|
return $targets;
|
|
}
|
|
|
|
/**
|
|
* Determine if a trade symbol represents ore cargo.
|
|
*
|
|
* @param string $tradeSymbol Trade symbol.
|
|
*
|
|
* @return bool
|
|
*/
|
|
function isOreTradeSymbol( string $tradeSymbol ): bool {
|
|
return preg_match( '/_ORE$/', strtoupper( $tradeSymbol ) ) === 1;
|
|
}
|
|
|
|
/**
|
|
* Determine which ship symbol should be used for contract negotiation.
|
|
*
|
|
* @param array<int,array<string,mixed>> $miningShips Mining ship records.
|
|
* @param array<int,array<string,mixed>> $ships All ship records.
|
|
*
|
|
* @return string
|
|
*/
|
|
function getContractNegotiationShipSymbol( array $miningShips, array $ships ): string {
|
|
if (! empty( $miningShips ) ) {
|
|
return (string) ( $miningShips[0]['symbol'] ?? '' );
|
|
}
|
|
|
|
if (! empty( $ships ) ) {
|
|
return (string) ( $ships[0]['symbol'] ?? '' );
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
$config = require __DIR__ . '/lib/project-config.php';
|
|
|
|
$storage = new SpacetradersStorage( $config['db_path'] );
|
|
$token = $storage->getAgentToken();
|
|
|
|
$statusMessage = '';
|
|
$errorMessage = '';
|
|
$actionResults = array();
|
|
$agent = array();
|
|
$ships = array();
|
|
$miningShips = array();
|
|
$activeContracts = array();
|
|
$marketWaypoints = array();
|
|
$contractDeliveryTargets = array();
|
|
$selectedMarketWaypoint = '';
|
|
$selectedContractDeliveryKey = '';
|
|
|
|
if (! is_string( $token ) || trim( $token ) === '' ) {
|
|
$envToken = getenv( 'SPACETRADERS_TOKEN' );
|
|
|
|
if (is_string( $envToken ) && trim( $envToken ) !== '' ) {
|
|
$token = trim( $envToken );
|
|
$storage->setAgentToken( $token );
|
|
}
|
|
}
|
|
|
|
if (! is_string( $token ) || trim( $token ) === '' ) {
|
|
$tokenError = 'No token found. Set one in config.php or SPACETRADERS_TOKEN.';
|
|
}
|
|
|
|
if (! isset( $tokenError ) ) {
|
|
$client = new SpacetradersApi(
|
|
trim( $token ),
|
|
$config['api_base_url'],
|
|
(int) $config['api_timeout'],
|
|
$storage,
|
|
(int) $config['cache_ttl']
|
|
);
|
|
}
|
|
|
|
try {
|
|
if (! isset( $tokenError ) ) {
|
|
$agentResponse = $client->getMyAgent();
|
|
$shipsResponse = $client->listMyShips();
|
|
$contractsResponse = $client->listMyContracts();
|
|
|
|
$agent = $agentResponse['data'] ?? $agentResponse;
|
|
$ships = $shipsResponse['data'] ?? $shipsResponse;
|
|
$contracts = $contractsResponse['data'] ?? $contractsResponse;
|
|
$miningShips = filterMiningShips( $ships );
|
|
$activeContracts = array_values(
|
|
array_filter(
|
|
(array) $contracts,
|
|
static function ( array $contract ): bool {
|
|
return (bool) ( $contract['accepted'] ?? false ) && ! (bool) ( $contract['fulfilled'] ?? false );
|
|
}
|
|
)
|
|
);
|
|
|
|
$currentSystemSymbol = '';
|
|
if (! empty( $miningShips ) && isset( $miningShips[0]['nav']['systemSymbol'] ) ) {
|
|
$currentSystemSymbol = (string) $miningShips[0]['nav']['systemSymbol'];
|
|
} elseif (! empty( $ships ) && isset( $ships[0]['nav']['systemSymbol'] ) ) {
|
|
$currentSystemSymbol = (string) $ships[0]['nav']['systemSymbol'];
|
|
} elseif (isset( $agent['headquarters'] ) && is_string( $agent['headquarters'] ) ) {
|
|
$hqParts = explode( '-', $agent['headquarters'] );
|
|
if (count( $hqParts ) >= 2 ) {
|
|
$currentSystemSymbol = $hqParts[0] . '-' . $hqParts[1];
|
|
}
|
|
}
|
|
|
|
if ($currentSystemSymbol !== '' ) {
|
|
$page = 1;
|
|
$total = 0;
|
|
$waypoints = array();
|
|
do {
|
|
$waypointsResponse = $client->listWaypoints(
|
|
$currentSystemSymbol,
|
|
array(
|
|
'page' => $page,
|
|
'limit' => 20,
|
|
)
|
|
);
|
|
$pageData = $waypointsResponse['data'] ?? array();
|
|
if (! is_array( $pageData ) || empty( $pageData ) ) {
|
|
break;
|
|
}
|
|
|
|
$waypoints = array_merge( $waypoints, $pageData );
|
|
$total = (int) ( $waypointsResponse['meta']['total'] ?? count( $waypoints ) );
|
|
$page++;
|
|
} while (count( $waypoints ) < $total);
|
|
|
|
foreach ( $waypoints as $waypoint ) {
|
|
foreach ( (array) ( $waypoint['traits'] ?? array() ) as $trait ) {
|
|
if ((string) ( $trait['symbol'] ?? '' ) === 'MARKETPLACE' ) {
|
|
$marketWaypoints[] = (string) ( $waypoint['symbol'] ?? '' );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$selectedMarketWaypoint = isset( $_POST['market_waypoint'] ) ?
|
|
trim( (string) $_POST['market_waypoint'] ) :
|
|
( $marketWaypoints[0] ?? '' );
|
|
$contractDeliveryTargets = buildContractDeliveryTargets( $activeContracts );
|
|
$selectedContractDeliveryKey = isset( $_POST['contract_delivery_key'] ) ?
|
|
trim( (string) $_POST['contract_delivery_key'] ) :
|
|
(string) ( $contractDeliveryTargets[0]['key'] ?? '' );
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['contract_action'] ) ) {
|
|
$contractAction = (string) $_POST['contract_action'];
|
|
|
|
if ($contractAction === 'fulfill_contract' ) {
|
|
$contractId = trim( (string) ( $_POST['contract_id'] ?? '' ) );
|
|
|
|
if ($contractId !== '' ) {
|
|
try {
|
|
$client->fulfillContract( $contractId );
|
|
$statusMessage = 'Contract ' . $contractId . ' fulfilled.';
|
|
$storage->clearAllCache();
|
|
|
|
$shipsResponse = $client->listMyShips();
|
|
$contractsResponse = $client->listMyContracts();
|
|
$ships = $shipsResponse['data'] ?? $shipsResponse;
|
|
$contracts = $contractsResponse['data'] ?? $contractsResponse;
|
|
$miningShips = filterMiningShips( $ships );
|
|
$activeContracts = array_values(
|
|
array_filter(
|
|
(array) $contracts,
|
|
static function ( array $contract ): bool {
|
|
return (bool) ( $contract['accepted'] ?? false ) && ! (bool) ( $contract['fulfilled'] ?? false );
|
|
}
|
|
)
|
|
);
|
|
$contractDeliveryTargets = buildContractDeliveryTargets( $activeContracts );
|
|
$selectedContractDeliveryKey = (string) ( $contractDeliveryTargets[0]['key'] ?? '' );
|
|
} catch (SpacetradersApiException $e) {
|
|
$errorMessage = 'Unable to fulfill contract: ' . $e->getMessage();
|
|
}
|
|
}
|
|
} elseif ($contractAction === 'negotiate_contract' ) {
|
|
$negotiateShipSymbol = trim( (string) ( $_POST['negotiate_ship_symbol'] ?? '' ) );
|
|
if ($negotiateShipSymbol === '' ) {
|
|
$negotiateShipSymbol = getContractNegotiationShipSymbol( $miningShips, $ships );
|
|
}
|
|
|
|
if ($negotiateShipSymbol === '' ) {
|
|
$errorMessage = 'No available ship found to negotiate a contract.';
|
|
} else {
|
|
try {
|
|
$client->negotiateContract( $negotiateShipSymbol );
|
|
$statusMessage = 'Negotiated a new contract using ' . $negotiateShipSymbol . '.';
|
|
$storage->clearAllCache();
|
|
|
|
$shipsResponse = $client->listMyShips();
|
|
$contractsResponse = $client->listMyContracts();
|
|
$ships = $shipsResponse['data'] ?? $shipsResponse;
|
|
$contracts = $contractsResponse['data'] ?? $contractsResponse;
|
|
$miningShips = filterMiningShips( $ships );
|
|
$activeContracts = array_values(
|
|
array_filter(
|
|
(array) $contracts,
|
|
static function ( array $contract ): bool {
|
|
return (bool) ( $contract['accepted'] ?? false ) && ! (bool) ( $contract['fulfilled'] ?? false );
|
|
}
|
|
)
|
|
);
|
|
$contractDeliveryTargets = buildContractDeliveryTargets( $activeContracts );
|
|
$selectedContractDeliveryKey = (string) ( $contractDeliveryTargets[0]['key'] ?? '' );
|
|
} catch (SpacetradersApiException $e) {
|
|
$errorMessage = 'Unable to negotiate contract: ' . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['fleet_action'] ) ) {
|
|
$fleetAction = (string) $_POST['fleet_action'];
|
|
$successCount = 0;
|
|
$selectedContractDeliveryTarget = null;
|
|
$remainingContractUnits = 0;
|
|
|
|
foreach ( $contractDeliveryTargets as $target ) {
|
|
if ((string) ( $target['key'] ?? '' ) === $selectedContractDeliveryKey ) {
|
|
$selectedContractDeliveryTarget = $target;
|
|
$remainingContractUnits = (int) ( $target['remainingUnits'] ?? 0 );
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach ( $miningShips as $fleetShip ) {
|
|
$shipSymbol = (string) ( $fleetShip['symbol'] ?? '' );
|
|
if ($shipSymbol === '' ) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$shipResponse = $client->getShip( $shipSymbol );
|
|
$shipData = $shipResponse['data'] ?? array();
|
|
$shipStatus = (string) ( $shipData['nav']['status'] ?? '' );
|
|
$waypointSymbol = (string) ( $shipData['nav']['waypointSymbol'] ?? '' );
|
|
|
|
if ($fleetAction === 'excavate_all' ) {
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
$actionResults[] = $shipSymbol . ': skipped (in transit).';
|
|
continue;
|
|
}
|
|
|
|
if ($shipStatus === 'DOCKED' ) {
|
|
$client->orbitShip( $shipSymbol );
|
|
}
|
|
|
|
$client->extractResources( $shipSymbol );
|
|
$actionResults[] = $shipSymbol . ': extracting.';
|
|
$successCount++;
|
|
} elseif ($fleetAction === 'dock_refuel_all' ) {
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
$actionResults[] = $shipSymbol . ': skipped (in transit).';
|
|
continue;
|
|
}
|
|
|
|
if ($shipStatus !== 'DOCKED' ) {
|
|
$client->dockShip( $shipSymbol );
|
|
}
|
|
|
|
$client->refuelShip( $shipSymbol );
|
|
$actionResults[] = $shipSymbol . ': docked/refueled.';
|
|
$successCount++;
|
|
} elseif ($fleetAction === 'move_and_sell_all' ) {
|
|
if ($selectedMarketWaypoint === '' ) {
|
|
$actionResults[] = $shipSymbol . ': no market waypoint selected.';
|
|
continue;
|
|
}
|
|
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
$actionResults[] = $shipSymbol . ': already in transit.';
|
|
continue;
|
|
}
|
|
|
|
if ($waypointSymbol !== $selectedMarketWaypoint ) {
|
|
if ($shipStatus === 'DOCKED' ) {
|
|
$client->orbitShip( $shipSymbol );
|
|
}
|
|
|
|
$client->navigateShip( $shipSymbol, $selectedMarketWaypoint );
|
|
$actionResults[] = $shipSymbol . ': navigating to ' . $selectedMarketWaypoint . '.';
|
|
$successCount++;
|
|
continue;
|
|
}
|
|
|
|
if ($shipStatus !== 'DOCKED' ) {
|
|
$client->dockShip( $shipSymbol );
|
|
}
|
|
|
|
$inventory = (array) ( $shipData['cargo']['inventory'] ?? array() );
|
|
$soldItems = 0;
|
|
foreach ( $inventory as $item ) {
|
|
$tradeSymbol = (string) ( $item['symbol'] ?? '' );
|
|
$units = (int) ( $item['units'] ?? 0 );
|
|
if ($tradeSymbol === '' || $units <= 0 ) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$client->sellCargo( $shipSymbol, $tradeSymbol, $units );
|
|
$soldItems++;
|
|
} catch (SpacetradersApiException $e) {
|
|
// Ignore individual sell failures and continue.
|
|
}
|
|
}
|
|
|
|
$actionResults[] = $shipSymbol . ': sold ' . $soldItems . ' cargo item type(s).';
|
|
$successCount++;
|
|
} elseif ($fleetAction === 'sell_cargo_here_all' ) {
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
$actionResults[] = $shipSymbol . ': skipped (in transit).';
|
|
continue;
|
|
}
|
|
|
|
if ($shipStatus !== 'DOCKED' ) {
|
|
$client->dockShip( $shipSymbol );
|
|
}
|
|
|
|
$inventory = (array) ( $shipData['cargo']['inventory'] ?? array() );
|
|
$soldItems = 0;
|
|
foreach ( $inventory as $item ) {
|
|
$tradeSymbol = (string) ( $item['symbol'] ?? '' );
|
|
$units = (int) ( $item['units'] ?? 0 );
|
|
if ($tradeSymbol === '' || $units <= 0 ) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$client->sellCargo( $shipSymbol, $tradeSymbol, $units );
|
|
$soldItems++;
|
|
} catch (SpacetradersApiException $e) {
|
|
// Continue selling other items.
|
|
}
|
|
}
|
|
|
|
$actionResults[] = $shipSymbol . ': sold ' . $soldItems . ' cargo item type(s) at current waypoint.';
|
|
$successCount++;
|
|
} elseif ($fleetAction === 'jettison_cargo_all' ) {
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
$actionResults[] = $shipSymbol . ': skipped (in transit).';
|
|
continue;
|
|
}
|
|
|
|
$inventory = (array) ( $shipData['cargo']['inventory'] ?? array() );
|
|
$jettisonedItems = 0;
|
|
foreach ( $inventory as $item ) {
|
|
$tradeSymbol = (string) ( $item['symbol'] ?? '' );
|
|
$units = (int) ( $item['units'] ?? 0 );
|
|
if ($tradeSymbol === '' || $units <= 0 ) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$client->jettisonCargo( $shipSymbol, $tradeSymbol, $units );
|
|
$jettisonedItems++;
|
|
} catch (SpacetradersApiException $e) {
|
|
// Continue jettisoning other items.
|
|
}
|
|
}
|
|
|
|
$actionResults[] = $shipSymbol . ': jettisoned ' . $jettisonedItems . ' cargo item type(s).';
|
|
$successCount++;
|
|
} elseif ($fleetAction === 'jettison_non_ore_cargo_all' ) {
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
$actionResults[] = $shipSymbol . ': skipped (in transit).';
|
|
continue;
|
|
}
|
|
|
|
$inventory = (array) ( $shipData['cargo']['inventory'] ?? array() );
|
|
$jettisonedItems = 0;
|
|
foreach ( $inventory as $item ) {
|
|
$tradeSymbol = (string) ( $item['symbol'] ?? '' );
|
|
$units = (int) ( $item['units'] ?? 0 );
|
|
if ($tradeSymbol === '' || $units <= 0 ) {
|
|
continue;
|
|
}
|
|
|
|
if (isOreTradeSymbol( $tradeSymbol ) ) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$client->jettisonCargo( $shipSymbol, $tradeSymbol, $units );
|
|
$jettisonedItems++;
|
|
} catch (SpacetradersApiException $e) {
|
|
// Continue jettisoning other items.
|
|
}
|
|
}
|
|
|
|
$actionResults[] = $shipSymbol . ': jettisoned ' . $jettisonedItems . ' non-ore cargo item type(s).';
|
|
$successCount++;
|
|
} elseif ($fleetAction === 'navigate_contract_delivery' ) {
|
|
if (! is_array( $selectedContractDeliveryTarget ) ) {
|
|
$actionResults[] = $shipSymbol . ': no contract delivery target selected.';
|
|
continue;
|
|
}
|
|
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
$actionResults[] = $shipSymbol . ': already in transit.';
|
|
continue;
|
|
}
|
|
|
|
$destinationSymbol = (string) ( $selectedContractDeliveryTarget['destinationSymbol'] ?? '' );
|
|
if ($destinationSymbol === '' ) {
|
|
$actionResults[] = $shipSymbol . ': delivery destination is missing.';
|
|
continue;
|
|
}
|
|
|
|
if ($waypointSymbol !== $destinationSymbol ) {
|
|
if ($shipStatus === 'DOCKED' ) {
|
|
$client->orbitShip( $shipSymbol );
|
|
}
|
|
|
|
$client->navigateShip( $shipSymbol, $destinationSymbol );
|
|
$actionResults[] = $shipSymbol . ': navigating to contract delivery waypoint ' . $destinationSymbol . '.';
|
|
$successCount++;
|
|
} else {
|
|
$actionResults[] = $shipSymbol . ': already at contract delivery waypoint.';
|
|
}
|
|
} elseif ($fleetAction === 'deliver_contract_goods' ) {
|
|
if (! is_array( $selectedContractDeliveryTarget ) ) {
|
|
$actionResults[] = $shipSymbol . ': no contract delivery target selected.';
|
|
continue;
|
|
}
|
|
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
$actionResults[] = $shipSymbol . ': skipped (in transit).';
|
|
continue;
|
|
}
|
|
|
|
$contractId = (string) ( $selectedContractDeliveryTarget['contractId'] ?? '' );
|
|
$destinationSymbol = (string) ( $selectedContractDeliveryTarget['destinationSymbol'] ?? '' );
|
|
$tradeSymbol = (string) ( $selectedContractDeliveryTarget['tradeSymbol'] ?? '' );
|
|
|
|
if ($contractId === '' || $destinationSymbol === '' || $tradeSymbol === '' ) {
|
|
$actionResults[] = $shipSymbol . ': selected contract delivery target is incomplete.';
|
|
continue;
|
|
}
|
|
|
|
if ($remainingContractUnits <= 0 ) {
|
|
$actionResults[] = $shipSymbol . ': nothing remaining to deliver for this target.';
|
|
continue;
|
|
}
|
|
|
|
if ($waypointSymbol !== $destinationSymbol ) {
|
|
$actionResults[] = $shipSymbol . ': not at delivery waypoint (' . $destinationSymbol . ').';
|
|
continue;
|
|
}
|
|
|
|
if ($shipStatus !== 'DOCKED' ) {
|
|
$client->dockShip( $shipSymbol );
|
|
}
|
|
|
|
$inventory = (array) ( $shipData['cargo']['inventory'] ?? array() );
|
|
$availableUnits = 0;
|
|
foreach ( $inventory as $item ) {
|
|
if ((string) ( $item['symbol'] ?? '' ) === $tradeSymbol ) {
|
|
$availableUnits = (int) ( $item['units'] ?? 0 );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($availableUnits <= 0 ) {
|
|
$actionResults[] = $shipSymbol . ': no ' . $tradeSymbol . ' in cargo.';
|
|
continue;
|
|
}
|
|
|
|
$deliverUnits = min( $availableUnits, $remainingContractUnits );
|
|
|
|
if ($deliverUnits <= 0 ) {
|
|
$actionResults[] = $shipSymbol . ': no units available to deliver.';
|
|
continue;
|
|
}
|
|
|
|
$client->deliverContractCargo( $contractId, $shipSymbol, $tradeSymbol, $deliverUnits );
|
|
$remainingContractUnits -= $deliverUnits;
|
|
$actionResults[] = $shipSymbol . ': delivered ' . $deliverUnits . ' of ' . $tradeSymbol . ' for contract ' . $contractId . '.';
|
|
$successCount++;
|
|
}
|
|
} catch (SpacetradersApiException $e) {
|
|
$actionResults[] = $shipSymbol . ': ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if ($successCount > 0 ) {
|
|
$storage->clearAllCache();
|
|
}
|
|
|
|
$statusMessage = 'Action complete for ' . $successCount . ' ship(s).';
|
|
|
|
$shipsResponse = $client->listMyShips();
|
|
$contractsResponse = $client->listMyContracts();
|
|
$ships = $shipsResponse['data'] ?? $shipsResponse;
|
|
$contracts = $contractsResponse['data'] ?? $contractsResponse;
|
|
$miningShips = filterMiningShips( $ships );
|
|
$activeContracts = array_values(
|
|
array_filter(
|
|
(array) $contracts,
|
|
static function ( array $contract ): bool {
|
|
return (bool) ( $contract['accepted'] ?? false ) && ! (bool) ( $contract['fulfilled'] ?? false );
|
|
}
|
|
)
|
|
);
|
|
$contractDeliveryTargets = buildContractDeliveryTargets( $activeContracts );
|
|
$selectedContractDeliveryKey = isset( $_POST['contract_delivery_key'] ) ?
|
|
trim( (string) $_POST['contract_delivery_key'] ) :
|
|
(string) ( $contractDeliveryTargets[0]['key'] ?? '' );
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['ship_action'] ) && in_array( (string) $_POST['ship_action'], array( 'sell_ship_cargo', 'jettison_ship_cargo', 'jettison_non_ore_ship_cargo' ), true ) && isset( $_POST['ship_symbol'] )) {
|
|
$shipAction = (string) $_POST['ship_action'];
|
|
$sellShipSymbol = trim( (string) $_POST['ship_symbol'] );
|
|
|
|
if ($sellShipSymbol !== '' ) {
|
|
try {
|
|
$shipResponse = $client->getShip( $sellShipSymbol );
|
|
$shipData = $shipResponse['data'] ?? array();
|
|
$shipStatus = (string) ( $shipData['nav']['status'] ?? '' );
|
|
|
|
if ($shipStatus === 'IN_TRANSIT' ) {
|
|
throw new SpacetradersApiException( 'Ship is in transit and cargo action is unavailable right now.' );
|
|
}
|
|
|
|
if ($shipAction === 'sell_ship_cargo' && $shipStatus !== 'DOCKED' ) {
|
|
$client->dockShip( $sellShipSymbol );
|
|
}
|
|
|
|
$inventory = (array) ( $shipData['cargo']['inventory'] ?? array() );
|
|
$handledItems = 0;
|
|
foreach ( $inventory as $item ) {
|
|
$tradeSymbol = (string) ( $item['symbol'] ?? '' );
|
|
$units = (int) ( $item['units'] ?? 0 );
|
|
if ($tradeSymbol === '' || $units <= 0 ) {
|
|
continue;
|
|
}
|
|
|
|
if ($shipAction === 'jettison_non_ore_ship_cargo' && isOreTradeSymbol( $tradeSymbol ) ) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
if ($shipAction === 'sell_ship_cargo' ) {
|
|
$client->sellCargo( $sellShipSymbol, $tradeSymbol, $units );
|
|
} else {
|
|
$client->jettisonCargo( $sellShipSymbol, $tradeSymbol, $units );
|
|
}
|
|
$handledItems++;
|
|
} catch (SpacetradersApiException $e) {
|
|
// Continue processing other items.
|
|
}
|
|
}
|
|
|
|
if ($shipAction === 'sell_ship_cargo' ) {
|
|
$statusMessage = $sellShipSymbol . ': sold ' . $handledItems . ' cargo item type(s).';
|
|
} elseif ($shipAction === 'jettison_non_ore_ship_cargo' ) {
|
|
$statusMessage = $sellShipSymbol . ': jettisoned ' . $handledItems . ' non-ore cargo item type(s).';
|
|
} else {
|
|
$statusMessage = $sellShipSymbol . ': jettisoned ' . $handledItems . ' cargo item type(s).';
|
|
}
|
|
$storage->clearAllCache();
|
|
|
|
$shipsResponse = $client->listMyShips();
|
|
$ships = $shipsResponse['data'] ?? $shipsResponse;
|
|
$miningShips = filterMiningShips( $ships );
|
|
} catch (SpacetradersApiException $e) {
|
|
$errorMessage = $sellShipSymbol . ': ' . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (SpacetradersApiException $e) {
|
|
$errorMessage = $e->getMessage();
|
|
}
|
|
|
|
$deliveryReadyByTradeSymbol = array();
|
|
foreach ( $miningShips as $miningShip ) {
|
|
$inventory = (array) ( $miningShip['cargo']['inventory'] ?? array() );
|
|
foreach ( $inventory as $item ) {
|
|
$tradeSymbol = (string) ( $item['symbol'] ?? '' );
|
|
$units = (int) ( $item['units'] ?? 0 );
|
|
if ($tradeSymbol === '' || $units <= 0 ) {
|
|
continue;
|
|
}
|
|
|
|
if (! isset( $deliveryReadyByTradeSymbol[ $tradeSymbol ] ) ) {
|
|
$deliveryReadyByTradeSymbol[ $tradeSymbol ] = 0;
|
|
}
|
|
|
|
$deliveryReadyByTradeSymbol[ $tradeSymbol ] += $units;
|
|
}
|
|
}
|
|
|
|
$contractNegotiationShipSymbol = getContractNegotiationShipSymbol( $miningShips, $ships );
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Spacetraders - Mining Fleet</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
</head>
|
|
|
|
<body class="container mx-auto px-4 py-8 bg-stone-800 text-gray-200">
|
|
<?php require __DIR__ . '/main-menu.php'; ?>
|
|
|
|
<h1 class="text-3xl font-bold mb-6 underline decoration-gray-300 w-full"><a href="mining-fleet.php">Spacetraders - Mining Fleet</a></h1>
|
|
|
|
<?php if (isset( $tokenError ) ) : ?>
|
|
<div class="mb-6 border border-red-500 p-4 rounded text-red-300">
|
|
<?php echo htmlspecialchars( $tokenError ); ?>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php exit; ?>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($statusMessage !== '' ) : ?>
|
|
<div class="mb-4 border border-green-500 p-4 rounded text-green-300">
|
|
<?php echo htmlspecialchars( $statusMessage ); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($errorMessage !== '' ) : ?>
|
|
<div class="mb-4 border border-red-500 p-4 rounded text-red-300">
|
|
<?php echo htmlspecialchars( $errorMessage ); ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="mb-6 border border-gray-600 rounded p-4">
|
|
<h2 class="text-xl font-bold mb-3">Global Controls</h2>
|
|
<form method="post" class="flex flex-wrap items-end gap-3">
|
|
<button type="submit" name="fleet_action" value="excavate_all" class="px-4 py-2 bg-emerald-700 rounded hover:bg-emerald-600">
|
|
Command All: Excavate
|
|
</button>
|
|
<button type="submit" name="fleet_action" value="dock_refuel_all" class="px-4 py-2 bg-blue-700 rounded hover:bg-blue-600">
|
|
Command All: Dock & Refuel
|
|
</button>
|
|
<button type="submit" name="fleet_action" value="jettison_cargo_all" class="px-4 py-2 bg-red-800 rounded hover:bg-red-700">
|
|
Command All: Jettison Cargo
|
|
</button>
|
|
<button type="submit" name="fleet_action" value="jettison_non_ore_cargo_all" class="px-4 py-2 bg-red-700 rounded hover:bg-red-600">
|
|
Command All: Jettison Non-Ore Cargo
|
|
</button>
|
|
|
|
<div class="w-full border-t border-gray-600 my-4 grow"></div>
|
|
|
|
<div>
|
|
<label for="market_waypoint" class="block text-xl mb-1">Market Waypoint</label>
|
|
<select id="market_waypoint" name="market_waypoint" class="px-3 py-2 rounded text-black min-w-64">
|
|
<?php foreach ( $marketWaypoints as $waypoint ) : ?>
|
|
<option value="<?php echo htmlspecialchars( $waypoint ); ?>" <?php echo $waypoint === $selectedMarketWaypoint ? 'selected' : ''; ?>>
|
|
<?php echo htmlspecialchars( $waypoint ); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<button type="submit" name="fleet_action" value="move_and_sell_all" class="px-4 py-2 bg-amber-700 rounded hover:bg-amber-600">
|
|
Command All: Move to Market & Sell
|
|
</button>
|
|
<button type="submit" name="fleet_action" value="sell_cargo_here_all" class="px-4 py-2 bg-rose-700 rounded hover:bg-rose-600">
|
|
Command All: Sell Cargo Here
|
|
</button>
|
|
|
|
<div class="w-full border-t border-gray-600 my-4 grow"></div>
|
|
|
|
<div class="">
|
|
<label for="contract_delivery_key" class="block text-xl mb-1">Contract Delivery Target</label>
|
|
<select id="contract_delivery_key" name="contract_delivery_key" class="px-3 py-2 rounded text-black min-w-80">
|
|
<?php if (empty( $contractDeliveryTargets ) ) : ?>
|
|
<option value="">No outstanding delivery targets</option>
|
|
<?php else : ?>
|
|
<?php foreach ( $contractDeliveryTargets as $target ) : ?>
|
|
<?php
|
|
$targetKey = (string) ( $target['key'] ?? '' );
|
|
$targetLabel = 'Contract ' .
|
|
(string) ( $target['contractId'] ?? '' ) .
|
|
' | ' .
|
|
(string) ( formatString( $target['tradeSymbol'] ?? '' ) ) .
|
|
' | Remaining ' .
|
|
number_format( (int) ( $target['remainingUnits'] ?? 0 ) ) .
|
|
' | To ' .
|
|
(string) ( $target['destinationSymbol'] ?? '' );
|
|
?>
|
|
<option value="<?php echo htmlspecialchars( $targetKey ); ?>" <?php echo $targetKey === $selectedContractDeliveryKey ? 'selected' : ''; ?>>
|
|
<?php echo htmlspecialchars( $targetLabel ); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<button type="submit" name="fleet_action" value="navigate_contract_delivery" class="px-4 py-2 bg-indigo-700 rounded hover:bg-indigo-600">
|
|
Command All: Navigate to Contract Delivery
|
|
</button>
|
|
<button type="submit" name="fleet_action" value="deliver_contract_goods" class="px-4 py-2 bg-violet-700 rounded hover:bg-violet-600">
|
|
Command All: Deliver Contract Goods
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if (! empty( $actionResults ) ) : ?>
|
|
<div class="mb-6 border border-gray-600 rounded p-4">
|
|
<h3 class="font-bold mb-2">Action Results</h3>
|
|
<ul class="list-disc list-inside text-sm text-gray-300">
|
|
<?php foreach ( $actionResults as $result ) : ?>
|
|
<li><?php echo htmlspecialchars( (string) $result ); ?></li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<p class="mb-4 text-gray-300">
|
|
Mining Ships: <span class="font-semibold"><?php echo number_format( count( $miningShips ) ); ?></span>
|
|
</p>
|
|
|
|
<div class="mb-6 border border-gray-600 rounded p-4">
|
|
<h2 class="text-xl font-bold mb-3">Active Contracts</h2>
|
|
<?php if (empty( $activeContracts ) ) : ?>
|
|
<p class="text-gray-300 mb-3">No active contracts.</p>
|
|
<?php if ($contractNegotiationShipSymbol !== '' ) : ?>
|
|
<form method="post">
|
|
<input type="hidden" name="contract_action" value="negotiate_contract">
|
|
<input type="hidden" name="negotiate_ship_symbol" value="<?php echo htmlspecialchars( $contractNegotiationShipSymbol ); ?>">
|
|
<button type="submit" class="px-3 py-2 bg-emerald-700 rounded hover:bg-emerald-600">
|
|
Negotiate New Contract
|
|
</button>
|
|
</form>
|
|
<p class="text-sm text-gray-400 mt-2">
|
|
Uses ship: <?php echo htmlspecialchars( $contractNegotiationShipSymbol ); ?>
|
|
</p>
|
|
<?php else : ?>
|
|
<p class="text-sm text-gray-400">No ships available to negotiate a contract.</p>
|
|
<?php endif; ?>
|
|
<?php else : ?>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<?php foreach ( $activeContracts as $contract ) : ?>
|
|
<?php
|
|
$contractType = (string) ( $contract['type'] ?? '' );
|
|
$deliveries = (array) ( $contract['terms']['deliver'] ?? array() );
|
|
?>
|
|
<div class="border border-gray-500 rounded p-3">
|
|
<p><span class="font-bold">Contract:</span> <?php echo htmlspecialchars( (string) ( formatString( $contract['id'] ?? '' ) ) ); ?></p>
|
|
<p><span class="font-bold">Type:</span> <?php echo htmlspecialchars( formatString( $contractType ) ); ?></p>
|
|
<p>
|
|
<span class="font-bold">Deadline:</span>
|
|
<?php echo htmlspecialchars( (string) date( 'Y-m-d H:i:s', strtotime( (string) ( $contract['terms']['deadline'] ?? '' ) ) ) ); ?>
|
|
</p>
|
|
<p>
|
|
<span class="font-bold">Payment:</span>
|
|
<?php echo number_format( (int) ( $contract['terms']['payment']['onAccepted'] ?? 0 ) ); ?>
|
|
+
|
|
<?php echo number_format( (int) ( $contract['terms']['payment']['onFulfilled'] ?? 0 ) ); ?>
|
|
</p>
|
|
<form method="post" class="mt-3">
|
|
<input type="hidden" name="contract_action" value="fulfill_contract">
|
|
<input type="hidden" name="contract_id" value="<?php echo htmlspecialchars( (string) ( $contract['id'] ?? '' ) ); ?>">
|
|
<button type="submit" class="px-3 py-1 bg-emerald-700 rounded hover:bg-emerald-600">
|
|
Fulfill Contract
|
|
</button>
|
|
</form>
|
|
<?php if (! empty( $deliveries ) ) : ?>
|
|
<p class="mt-2 font-bold">Deliveries:</p>
|
|
<ul class="list-disc list-inside text-sm text-gray-300">
|
|
<?php foreach ( $deliveries as $delivery ) : ?>
|
|
<?php
|
|
$tradeSymbol = (string) ( $delivery['tradeSymbol'] ?? '' );
|
|
$unitsRequired = (int) ( $delivery['unitsRequired'] ?? 0 );
|
|
$unitsFulfilled = (int) ( $delivery['unitsFulfilled'] ?? 0 );
|
|
$remainingUnits = max( 0, $unitsRequired - $unitsFulfilled );
|
|
$readyUnits = (int) ( $deliveryReadyByTradeSymbol[ $tradeSymbol ] ?? 0 );
|
|
$deliverableNow = min( $readyUnits, $remainingUnits );
|
|
?>
|
|
<li>
|
|
<?php echo htmlspecialchars( formatString( $tradeSymbol ) ); ?>:
|
|
<?php echo number_format( $unitsFulfilled ); ?>/<?php echo number_format( $unitsRequired ); ?>
|
|
to <?php echo htmlspecialchars( (string) ( $delivery['destinationSymbol'] ?? '' ) ); ?>
|
|
| Ready: <?php echo number_format( $deliverableNow ); ?>
|
|
<?php if ($readyUnits > $deliverableNow ) : ?>
|
|
(<?php echo number_format( $readyUnits ); ?> in cargo)
|
|
<?php endif; ?>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php if (empty( $miningShips ) ) : ?>
|
|
<div class="border border-gray-600 rounded p-4">No mining ships found (role: EXCAVATOR).</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<?php foreach ( $miningShips as $ship ) : ?>
|
|
<?php
|
|
$shipSymbol = (string) ( $ship['symbol'] ?? '' );
|
|
$fuelCurrent = (int) ( $ship['fuel']['current'] ?? 0 );
|
|
$fuelCapacity = (int) ( $ship['fuel']['capacity'] ?? 0 );
|
|
$cargoUnits = (int) ( $ship['cargo']['units'] ?? 0 );
|
|
$cargoCapacity = (int) ( $ship['cargo']['capacity'] ?? 0 );
|
|
$cooldownRemaining = (int) ( $ship['cooldown']['remainingSeconds'] ?? 0 );
|
|
$shipStatus = (string) ( $ship['nav']['status'] ?? '' );
|
|
$arrivalIso = (string) ( $ship['nav']['route']['arrival'] ?? '' );
|
|
?>
|
|
<div class="border border-gray-600 rounded p-4">
|
|
<h3 class="text-lg font-bold mb-2">
|
|
<a href="ship-details.php?ship=<?php echo urlencode( $shipSymbol ); ?>" class="text-blue-400 hover:underline">
|
|
<?php echo htmlspecialchars( formatString( $shipSymbol ) ); ?>
|
|
</a>
|
|
</h3>
|
|
<p><span class="font-bold">Status:</span> <?php echo htmlspecialchars( formatString( $shipStatus ) ); ?></p>
|
|
<p><span class="font-bold">Waypoint:</span> <?php echo htmlspecialchars( (string) ( $ship['nav']['waypointSymbol'] ?? '' ) ); ?></p>
|
|
<p><span class="font-bold">Fuel:</span> <?php echo number_format( $fuelCurrent ); ?>/<?php echo number_format( $fuelCapacity ); ?></p>
|
|
<p><span class="font-bold">Cargo:</span> <?php echo number_format( $cargoUnits ); ?>/<?php echo number_format( $cargoCapacity ); ?></p>
|
|
<p>
|
|
<span class="font-bold">Navigation Timer:</span>
|
|
<span
|
|
class="ship-nav-timer"
|
|
data-status="<?php echo htmlspecialchars( $shipStatus ); ?>"
|
|
data-arrival="<?php echo htmlspecialchars( $arrivalIso ); ?>"
|
|
></span>
|
|
</p>
|
|
<p>
|
|
<span class="font-bold">Cooldown:</span>
|
|
<span class="ship-cooldown" data-seconds="<?php echo htmlspecialchars( (string) max( 0, $cooldownRemaining ) ); ?>"></span>
|
|
</p>
|
|
|
|
<?php $inventory = (array) ( $ship['cargo']['inventory'] ?? array() ); ?>
|
|
<?php if (! empty( $inventory ) ) : ?>
|
|
<p class="mt-2 font-bold">Cargo Contents:</p>
|
|
<ul class="list-disc list-inside text-sm text-gray-300">
|
|
<?php foreach ( $inventory as $item ) : ?>
|
|
<li>
|
|
<?php echo htmlspecialchars( (string) ( formatString( $item['symbol'] ?? '' ) ) ); ?>:
|
|
<?php echo number_format( (int) ( $item['units'] ?? 0 ) ); ?>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
|
|
<form method="post" class="mt-3">
|
|
<input type="hidden" name="ship_action" value="sell_ship_cargo">
|
|
<input type="hidden" name="ship_symbol" value="<?php echo htmlspecialchars( $shipSymbol ); ?>">
|
|
<button type="submit" class="px-3 py-1 bg-rose-700 rounded hover:bg-rose-600">Sell This Ship's Cargo</button>
|
|
</form>
|
|
<form method="post" class="mt-2">
|
|
<input type="hidden" name="ship_action" value="jettison_ship_cargo">
|
|
<input type="hidden" name="ship_symbol" value="<?php echo htmlspecialchars( $shipSymbol ); ?>">
|
|
<button type="submit" class="px-3 py-1 bg-red-800 rounded hover:bg-red-700">Jettison This Ship's Cargo</button>
|
|
</form>
|
|
<form method="post" class="mt-2">
|
|
<input type="hidden" name="ship_action" value="jettison_non_ore_ship_cargo">
|
|
<input type="hidden" name="ship_symbol" value="<?php echo htmlspecialchars( $shipSymbol ); ?>">
|
|
<button type="submit" class="px-3 py-1 bg-red-700 rounded hover:bg-red-600">Jettison Non-Ore Cargo</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const cooldownNodes = document.querySelectorAll('.ship-cooldown');
|
|
const navTimerNodes = document.querySelectorAll('.ship-nav-timer');
|
|
|
|
function formatCooldown(seconds) {
|
|
if (seconds <= 0) {
|
|
return 'Ready';
|
|
}
|
|
|
|
const total = Math.max(0, Math.floor(seconds));
|
|
const mins = Math.floor(total / 60);
|
|
const secs = total % 60;
|
|
const paddedSecs = secs.toString().padStart(2, '0');
|
|
return mins > 0 ? `${mins}:${paddedSecs}` : `${secs}s`;
|
|
}
|
|
|
|
function formatNavTimer(seconds) {
|
|
if (seconds <= 0) {
|
|
return 'Arriving';
|
|
}
|
|
|
|
const total = Math.max(0, Math.floor(seconds));
|
|
const hours = Math.floor(total / 3600);
|
|
const minutes = Math.floor((total % 3600) / 60);
|
|
const secs = total % 60;
|
|
|
|
if (hours > 0) {
|
|
return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
}
|
|
|
|
return `${minutes}:${String(secs).padStart(2, '0')}`;
|
|
}
|
|
|
|
function render() {
|
|
cooldownNodes.forEach((node) => {
|
|
const seconds = Number(node.dataset.seconds || '0');
|
|
node.textContent = formatCooldown(seconds);
|
|
});
|
|
|
|
const nowMs = Date.now();
|
|
navTimerNodes.forEach((node) => {
|
|
const status = node.dataset.status || '';
|
|
const arrival = node.dataset.arrival || '';
|
|
|
|
if (status !== 'IN_TRANSIT') {
|
|
node.textContent = 'Ready';
|
|
return;
|
|
}
|
|
|
|
const arrivalMs = Date.parse(arrival);
|
|
if (!Number.isFinite(arrivalMs)) {
|
|
node.textContent = 'In transit';
|
|
return;
|
|
}
|
|
|
|
const remainingSeconds = Math.max(0, Math.floor((arrivalMs - nowMs) / 1000));
|
|
node.textContent = formatNavTimer(remainingSeconds);
|
|
});
|
|
}
|
|
|
|
render();
|
|
|
|
if (cooldownNodes.length === 0 && navTimerNodes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
window.setInterval(() => {
|
|
cooldownNodes.forEach((node) => {
|
|
const current = Number(node.dataset.seconds || '0');
|
|
const next = Math.max(0, current - 1);
|
|
node.dataset.seconds = String(next);
|
|
});
|
|
|
|
render();
|
|
}, 1000);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|