Characters can now relax on the beach sand to recover energy and health. The server clamps position to the shoreline, exposes beach geometry in the city payload, and the client renders animated waves, sand, stalls, chairs, and ambient NPCs. Includes Beach support class, new beach action endpoint, localization strings, and feature tests.
168 lines
5.2 KiB
PHP
Executable File
168 lines
5.2 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 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; other axes clamp to the city bounds.
|
|
*/
|
|
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;
|
|
$this->assertSame(Beach::sandTo($city) + 2, $this->character->pos_x);
|
|
$this->assertSame((float) $city->bounds_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);
|
|
}
|
|
}
|