diff --git a/index.php b/index.php index b94c2a2..f9480dc 100644 --- a/index.php +++ b/index.php @@ -264,8 +264,7 @@ try {

Spacetraders - Dashboard

- Agent:
- Credits: + Agent:

@@ -448,11 +447,16 @@ try { Role Type Status + Nav Timer Flight Mode Route + - - + + + + + @@ -536,6 +547,47 @@ try { (function() { const tabs = document.querySelectorAll('.system-tab'); const panels = document.querySelectorAll('.system-tab-panel'); + const navTimerNodes = document.querySelectorAll('.ship-nav-timer'); + + 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 renderNavTimers() { + 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); + }); + } function activateTab(targetId) { panels.forEach((panel) => { @@ -561,6 +613,11 @@ try { if (tabs.length > 0) { activateTab('tab-waypoints'); } + + renderNavTimers(); + if (navTimerNodes.length > 0) { + window.setInterval(renderNavTimers, 1000); + } })(); diff --git a/lib/spacetraders-api.php b/lib/spacetraders-api.php index 669a1bb..49542a3 100644 --- a/lib/spacetraders-api.php +++ b/lib/spacetraders-api.php @@ -291,6 +291,33 @@ class SpacetradersApi { ); } + /** + * Deliver cargo for a contract using a ship at the delivery destination. + * + * @param string $contractId The contract ID. + * @param string $shipSymbol The ship symbol delivering cargo. + * @param string $tradeSymbol The trade symbol being delivered. + * @param int $units The number of units to deliver. + * + * @return array + */ + public function deliverContractCargo( + string $contractId, + string $shipSymbol, + string $tradeSymbol, + int $units + ): array { + return $this->request( + 'POST', + '/my/contracts/' . rawurlencode( $contractId ) . '/deliver', + array( + 'shipSymbol' => $shipSymbol, + 'tradeSymbol' => $tradeSymbol, + 'units' => $units, + ) + ); + } + /** * Get a list of all systems in the universe. * diff --git a/main-menu.php b/main-menu.php index 90bd3a3..7ba02e1 100644 --- a/main-menu.php +++ b/main-menu.php @@ -1,11 +1,81 @@ -
- Dashboard - | - Markets - | - Buy Ships - | - Mining Fleet - | - Configuration + + * @license MIT License + * @version GIT: + * @link https://git.keithsolomon.net/keith/Spacetraders + */ + +require_once __DIR__ . '/lib/spacetraders-api.php'; +require_once __DIR__ . '/lib/spacetraders-storage.php'; + +$config = require __DIR__ . '/lib/project-config.php'; + +$storage = new SpacetradersStorage( $config['db_path'] ); +$token = $storage->getAgentToken(); +$statusMessage = ''; +$errorMessage = ''; + +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.'; +} + +$agent = array(); + +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(); + $agent = $agentResponse['data'] ?? $agentResponse; + } +} catch (SpacetradersApiException $e) { + $error = array( + 'error' => $e->getMessage(), + 'code' => $e->getCode(), + 'payload' => $e->getErrorPayload(), + ); + + http_response_code( 500 ); + header( 'Content-Type: application/json; charset=utf-8' ); + echo json_encode( $error, JSON_PRETTY_PRINT ); +} +?> + +
+
+ Dashboard + | + Markets + | + Buy Ships + | + Mining Fleet + | + Configuration +
+ +
+ Credits: +
diff --git a/mining-fleet.php b/mining-fleet.php index 2e0ed02..6c87720 100644 --- a/mining-fleet.php +++ b/mining-fleet.php @@ -31,6 +31,70 @@ function filterMiningShips( array $ships ): array { ); } +/** + * Build contract delivery targets for outstanding delivery requirements. + * + * @param array> $activeContracts + * + * @return array> + */ +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; +} + $config = require __DIR__ . '/lib/project-config.php'; $storage = new SpacetradersStorage( $config['db_path'] ); @@ -44,7 +108,9 @@ $ships = array(); $miningShips = array(); $activeContracts = array(); $marketWaypoints = array(); +$contractDeliveryTargets = array(); $selectedMarketWaypoint = ''; +$selectedContractDeliveryKey = ''; if (! is_string( $token ) || trim( $token ) === '' ) { $envToken = getenv( 'SPACETRADERS_TOKEN' ); @@ -135,10 +201,24 @@ try { $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['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'] ?? '' ); @@ -277,6 +357,93 @@ try { $actionResults[] = $shipSymbol . ': jettisoned ' . $jettisonedItems . ' 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(); @@ -302,6 +469,10 @@ try { } ) ); + $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' ), true ) && isset( $_POST['ship_symbol'] )) { @@ -362,6 +533,34 @@ try { } catch (SpacetradersApiException $e) { $errorMessage = $e->getMessage(); } + +/** + * Format a string by replacing underscores with spaces and capitalizing the first letter. + * + * @param string $value The string to format. + * @return string The formatted string. + */ +function formatString( $value ): string { + return ucfirst( strtolower( str_replace( '_', ' ', $value ) ) ); +} + +$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; + } +} ?> @@ -413,6 +612,32 @@ try {
+
+ + +
+ @@ -428,6 +653,12 @@ try { + + @@ -458,8 +689,8 @@ try { $deliveries = (array) ( $contract['terms']['deliver'] ?? array() ); ?>
-

Contract:

-

Type:

+

Contract:

+

Type:

Deadline: @@ -475,13 +706,21 @@ try {

@@ -504,18 +743,32 @@ try { $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'] ?? '' ); ?>

- +

-

Name:

-

Status:

+

Status:

Waypoint:

Fuel: /

Cargo: /

+

+ Navigation Timer: + +

+

+ Cooldown: + +

@@ -523,7 +776,7 @@ try {
  • - : + :
  • @@ -543,5 +796,84 @@ try {
+ + diff --git a/ship-details.php b/ship-details.php index a720f25..d036b19 100644 --- a/ship-details.php +++ b/ship-details.php @@ -136,7 +136,7 @@ try { } $client->sellCargo( $shipSymbol, $tradeSymbol, $units ); - $statusMessage = 'Sold ' . number_format( $units ) . ' units of ' . $tradeSymbol . '.'; + $statusMessage = 'Sold ' . number_format( $units ) . ' units of ' . formatString( $tradeSymbol ) . '.'; break; case 'jettison_all_cargo': $shipResponse = $client->getShip( $shipSymbol ); @@ -180,7 +180,7 @@ try { } $client->jettisonCargo( $shipSymbol, $tradeSymbol, $units ); - $statusMessage = 'Jettisoned ' . number_format( $units ) . ' units of ' . $tradeSymbol . '.'; + $statusMessage = 'Jettisoned ' . number_format( $units ) . ' units of ' . formatString( $tradeSymbol ) . '.'; break; } @@ -196,6 +196,16 @@ try { } catch (SpacetradersApiException $e) { $errorMessage = $e->getMessage(); } + +/** + * Formats a string by replacing underscores with spaces and capitalizing the first letter. + * + * @param string $str The string to format. + * @return string The formatted string. + */ +function formatString( $str ) { + return ucfirst( strtolower( str_replace( '_', ' ', $str ) ) ); +} ?> @@ -240,7 +250,7 @@ try { @@ -289,8 +299,8 @@ try {

- Role: | - Status: | + Role: | + Status: | Fuel: /

@@ -298,18 +308,26 @@ try {

Overview

-

Symbol:

-

Name:

-

Role:

-

Faction:

+

Symbol:

+

Name:

+

Role:

+

Faction:

Navigation

-

Status:

-

Flight Mode:

+

Status:

+

Flight Mode:

System:

Waypoint:

+

+ Navigation Timer: + +

@@ -363,7 +381,6 @@ try { - @@ -373,7 +390,6 @@ try { - @@ -417,5 +433,56 @@ try { + +
Symbol Name Units Description