Files
City10/tests/Feature/CityActionsTest.php
marcocosta10000 580c830ffb feat(port): add sea port with quay, terminal, berths, and AI port master
Adds a new port district on the north-east coast with walkable quay, container yard, terminal, gantry cranes, and berths. Includes AI-driven port master (Capitão do Porto) and dock workers for ship dispatch, plus full frontend rendering of ships, boats, and trucks.
2026-10-31 23:33:06 -03:00

172 lines
5.4 KiB
PHP
Executable File

<?php
namespace Tests\Feature;
use App\Models\Building;
use App\Models\Character;
use App\Models\InventoryItem;
use App\Models\Job;
use App\Models\Transaction;
use App\Models\User;
use App\Support\Beach;
use App\Support\Port;
use Database\Seeders\CityWorldSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Player lifecycle actions and city data payload.
*/
class CityActionsTest extends TestCase
{
use RefreshDatabase;
/**
* Seed world and create an approved user with a character.
*/
protected Character $character;
protected User $user;
/**
* Boot world fixtures.
*/
protected function setUp(): void
{
parent::setUp();
$this->seed(CityWorldSeeder::class);
$this->user = User::factory()->create(['status' => User::STATUS_ACTIVE, 'is_active' => true]);
$this->character = Character::ensureForUser($this->user);
$this->actingAs($this->user);
}
/**
* City payload includes bounds, characters, jobs and own character.
*/
public function test_city_data_payload(): void
{
$response = $this->getJson('/city/data');
$response->assertOk()
->assertJsonPath('city.slug', 'city10')
->assertJsonStructure([
'city' => ['bounds' => ['min_x', 'max_x', 'min_z', 'max_z']],
'characters', 'buildings', 'jobs', 'me', 'layout' => ['roads', 'blocks'],
]);
$this->assertTrue($response->json('me.id') === $this->character->id);
$this->assertNotEmpty($response->json('layout.roads'));
}
/**
* Position updates clamp to the beach shoreline (past the road grid)
* and persist; Z clamps to the port strip (opened for x >= 150, the
* north-east port/coast) or to the city bounds otherwise.
*/
public function test_position_persists_and_clamps(): void
{
$this->postJson('/city/actions/position', ['pos_x' => 9999, 'pos_z' => -9999])
->assertOk();
$this->character->refresh();
$city = $this->character->city;
// Clamped X lands on the shoreline/quay band (>= VISIT_MIN_X), so
// the deep north coast (port strip) is reachable.
$this->assertSame(Beach::sandTo($city) + 2, $this->character->pos_x);
$this->assertSame(Port::VISIT_MIN_Z, $this->character->pos_z);
}
/**
* Eating requires food inventory and restores hunger.
*/
public function test_eat_consumes_food(): void
{
$this->character->forceFill(['hunger' => 40])->save();
InventoryItem::updateOrCreate(
['character_id' => $this->character->id, 'item' => 'food'],
['qty' => 1],
);
$this->postJson('/city/actions/eat')->assertOk();
$this->character->refresh();
$this->assertGreaterThan(40, $this->character->hunger);
$this->assertSame(0, (int) InventoryItem::where('character_id', $this->character->id)->where('item', 'food')->value('qty'));
}
/**
* Sleep is rejected without a completed house.
*/
public function test_sleep_requires_house(): void
{
$this->postJson('/city/actions/sleep')->assertStatus(422);
}
/**
* Applying a job attaches employment.
*/
public function test_apply_job(): void
{
$job = Job::firstOrFail();
$this->postJson('/city/actions/apply-job', ['job_id' => $job->id])->assertOk();
$this->character->refresh();
$this->assertSame($job->id, $this->character->job_id);
}
/**
* Buying moves money into inventory and records a transaction.
*/
public function test_buy_food(): void
{
$this->character->forceFill(['money' => 50])->save();
$this->postJson('/city/actions/buy', [
'item' => 'food',
'qty' => 2,
'price' => 5,
])->assertOk();
$this->character->refresh();
$this->assertSame(40.0, (float) $this->character->money);
// Starter kit provides 3 food; buying 2 more yields 5.
$this->assertSame(5, (int) InventoryItem::where('character_id', $this->character->id)->where('item', 'food')->value('qty'));
$this->assertSame(1, Transaction::count());
}
/**
* Building requires materials and free terrain.
*/
public function test_build_house(): void
{
InventoryItem::updateOrCreate(
['character_id' => $this->character->id, 'item' => 'materials'],
['qty' => 50],
);
$this->postJson('/city/actions/build', [
'pos_x' => 60,
'pos_z' => 60,
'name' => 'Minha casa',
])->assertOk();
$building = Building::where('owner_character_id', $this->character->id)->first();
$this->assertNotNull($building);
$this->assertSame(0, $building->build_progress);
$this->assertSame(0, (int) InventoryItem::where('character_id', $this->character->id)->where('item', 'materials')->value('qty'));
}
/**
* Second house on the same lot is rejected.
*/
public function test_build_rejects_occupied_site(): void
{
InventoryItem::updateOrCreate(
['character_id' => $this->character->id, 'item' => 'materials'],
['qty' => 100],
);
$payload = ['pos_x' => -60, 'pos_z' => 20, 'name' => 'A'];
$this->postJson('/city/actions/build', $payload)->assertOk();
$this->postJson('/city/actions/build', $payload)->assertStatus(422);
}
}