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.
172 lines
5.4 KiB
PHP
Executable File
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);
|
|
}
|
|
}
|