feature: Added ship details and mining fleet pages

This commit is contained in:
Keith Solomon
2026-02-10 06:23:15 -06:00
parent 58353b249a
commit 2424f87185
6 changed files with 1703 additions and 2 deletions

552
mining-fleet.php Normal file
View File

@@ -0,0 +1,552 @@
<?php
/**
* Spacetraders mining fleet control page.
*
* @package SpacetradersApi
* @author Keith Solomon <keith@keithsolomon.net>
* @license MIT License
* @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 spacetraders_filter_mining_ships( array $ships ): array {
return array_values(
array_filter(
$ships,
static function ( array $ship ): bool {
$role = strtoupper( (string) ( $ship['registration']['role'] ?? '' ) );
return $role === 'EXCAVATOR';
}
)
);
}
$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();
$selectedMarketWaypoint = '';
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 = spacetraders_filter_mining_ships( $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] ?? '' );
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['fleet_action'] ) ) {
$fleetAction = (string) $_POST['fleet_action'];
$successCount = 0;
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++;
}
} 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 = spacetraders_filter_mining_ships( $ships );
$activeContracts = array_values(
array_filter(
(array) $contracts,
static function ( array $contract ): bool {
return (bool) ( $contract['accepted'] ?? false ) && ! (bool) ( $contract['fulfilled'] ?? false );
}
)
);
}
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'] )
) {
$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;
}
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).';
} else {
$statusMessage = $sellShipSymbol . ': jettisoned ' . $handledItems . ' cargo item type(s).';
}
$storage->clearAllCache();
$shipsResponse = $client->listMyShips();
$ships = $shipsResponse['data'] ?? $shipsResponse;
$miningShips = spacetraders_filter_mining_ships( $ships );
} catch (SpacetradersApiException $e) {
$errorMessage = $sellShipSymbol . ': ' . $e->getMessage();
}
}
}
}
} catch (SpacetradersApiException $e) {
$errorMessage = $e->getMessage();
}
?>
<!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">
<div class="mb-6 flex gap-4">
<a href="agent-info.php" class="text-blue-400 hover:underline">Agent Info</a>
<a href="buy-ships.php" class="text-blue-400 hover:underline">Buy Ships</a>
<a href="config.php" class="text-blue-400 hover:underline">Configuration</a>
</div>
<h1 class="text-3xl font-bold mb-6 underline decoration-gray-300 w-full">Mining Fleet</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">
<div>
<label for="market_waypoint" class="block text-sm 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="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="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>
<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>
</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">No active contracts.</p>
<?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) ( $contract['id'] ?? '' ) ); ?></p>
<p><span class="font-bold">Type:</span> <?php echo htmlspecialchars( $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>
<?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
$unitsRequired = (int) ( $delivery['unitsRequired'] ?? 0 );
$unitsFulfilled = (int) ( $delivery['unitsFulfilled'] ?? 0 );
?>
<li>
<?php echo htmlspecialchars( (string) ( $delivery['tradeSymbol'] ?? '' ) ); ?>:
<?php echo number_format( $unitsFulfilled ); ?>/<?php echo number_format( $unitsRequired ); ?>
to <?php echo htmlspecialchars( (string) ( $delivery['destinationSymbol'] ?? '' ) ); ?>
</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 );
?>
<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( $shipSymbol ); ?>
</a>
</h3>
<p><span class="font-bold">Name:</span> <?php echo htmlspecialchars( (string) ( $ship['registration']['name'] ?? '' ) ); ?></p>
<p><span class="font-bold">Status:</span> <?php echo htmlspecialchars( (string) ( $ship['nav']['status'] ?? '' ) ); ?></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>
<?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) ( $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>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</body>
</html>