Files
City10/tests/Feature/CityActionsTest.php
T
marcocosta10000 79ce2a1825 feat(world): add persistent citizen simulation with needs, jobs, and social features
- Adds Character model with survival needs (hunger, thirst, energy) and state machine
- Implements city:tick world ticker for offline progression every minute
- Adds city territory expansion and building construction system
- Introduces job economy, inventory, and transaction audit trail
- Adds social graph (friends, marriage, children) and character chat
- Implements admin approval workflow for new registrations
- Adds hospital admission system for traffic accidents
- Includes comprehensive feature tests and world seeders
2026-10-17 21:46:23 -03:00

166 lines
5.1 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 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 inside city bounds and persist.
*/
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((float) $city->bounds_max_x, $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);
}
}