- 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
166 lines
5.1 KiB
PHP
Executable File
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);
|
|
}
|
|
}
|