Files
City10/tests/Feature/WorldFeaturesTest.php
T
marcocosta10000 a620f83dd8 feat(city): add player marketplace, mission journal, character customization and presence
- Player-to-player marketplace with listings, buying and selling
- Mission system with progress tracking and money rewards
- Character appearance customization (skin, hair, face, beard, accessory)
- Multiplayer presence feed showing nearby citizens in near-real-time
- Admin world dashboard with live platform metrics
- Bus and train transportation with temporary ride state
- Mobile joystick controls and minimap HUD
- Education bonus applied to work salary
2026-10-17 22:51:20 -03:00

178 lines
5.4 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Character;
use App\Models\InventoryItem;
use App\Models\LegalDocument;
use App\Models\MarketListing;
use App\Models\Mission;
use App\Models\Transaction;
use App\Models\User;
use App\Models\UserConsent;
use App\Support\CityPlazas;
use Database\Seeders\CityWorldSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* Presence, marketplace, missions, customize and ride endpoints.
*/
class WorldFeaturesTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Character $character;
/**
* 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);
}
/**
* Presence lists citizens in the city.
*/
public function test_presence_returns_citizens(): void
{
$this->character->forceFill(['is_online' => true, 'pos_x' => 5, 'pos_z' => 5])->save();
$this->getJson('/city/presence')
->assertOk()
->assertJsonStructure(['city_id', 'server_time', 'citizens' => [['id', 'name', 'position', 'is_me']]]);
}
/**
* Market sell lists inventory and buy transfers money.
*/
public function test_market_sell_and_buy(): void
{
InventoryItem::updateOrCreate(
['character_id' => $this->character->id, 'item' => 'product'],
['qty' => 3],
);
$this->postJson('/city/market/sell', [
'item' => 'product',
'qty' => 2,
'unit_price' => 5,
])->assertOk();
$listing = MarketListing::firstOrFail();
$this->assertSame(1, (int) InventoryItem::where('character_id', $this->character->id)->where('item', 'product')->value('qty'));
$buyerUser = User::factory()->create(['status' => User::STATUS_ACTIVE, 'is_active' => true]);
$buyer = Character::ensureForUser($buyerUser);
$buyer->forceFill(['money' => 50])->save();
$buy = $this->actingAs($buyerUser)
->postJson('/city/market/buy', ['listing_id' => $listing->id, 'qty' => 2]);
$buy->assertOk();
$this->assertEqualsWithDelta(40.0, (float) $buy->json('money'), 0.01);
$this->assertFalse($listing->fresh()->is_active);
$this->assertSame(1, Transaction::count());
}
/**
* Completing exercise metric finishes the exercise mission with reward.
*/
public function test_mission_completes_on_exercise(): void
{
$mission = Mission::where('code', 'exercise-5')->firstOrFail();
$eq = CityPlazas::forCity($this->character->city)[0]['equipment'][0];
$this->character->forceFill([
'pos_x' => $eq['position'][0],
'pos_z' => $eq['position'][1],
'health' => 50,
'energy' => 90,
'exercise_count' => 4,
'money' => 10,
])->save();
$this->postJson('/city/actions/exercise', [
'pos_x' => $eq['position'][0],
'pos_z' => $eq['position'][1],
])->assertOk()
->assertJsonPath('character.exercise_count', 5);
$pivot = DB::table('character_missions')
->where('character_id', $this->character->id)
->where('mission_id', $mission->id)
->first();
$this->assertNotNull($pivot);
$this->assertTrue((bool) $pivot->is_completed);
$this->assertSame(10.0 + 25.0, round((float) $this->character->fresh()->money, 2));
}
/**
* Appearance customization persists safe slots.
*/
public function test_customize_updates_appearance(): void
{
$this->putJson('/city/customize', [
'skin' => 2,
'hair' => 3,
'shirt' => 4,
'face' => 1,
'beard' => 0,
'accessory' => 2,
])->assertOk()
->assertJsonPath('appearance.skin', 2);
$this->character->refresh();
$this->assertSame(3, (int) $this->character->hair);
}
/**
* Boarding a bus requires being near a road.
*/
public function test_ride_bus_requires_road(): void
{
$this->postJson('/city/actions/ride', [
'kind' => 'bus',
'pos_x' => 10,
'pos_z' => 10,
])->assertStatus(422);
$this->postJson('/city/actions/ride', [
'kind' => 'bus',
'pos_x' => 1,
'pos_z' => 0,
])->assertOk()
->assertJsonPath('character.ride_kind', 'bus');
}
/**
* Admin world dashboard metrics endpoint.
*/
public function test_admin_world_metrics(): void
{
$admin = User::factory()->admin()->create();
foreach (LegalDocument::where('is_current', true)->pluck('id') as $docId) {
UserConsent::create([
'user_id' => $admin->id,
'legal_document_id' => $docId,
'accepted_at' => now(),
'ip_address' => '127.0.0.1',
'user_agent' => 'phpunit',
'source' => 'web_form',
]);
}
$this->actingAs($admin)
->get('/admin/world')
->assertOk()
->assertSee(__('admin.world.title'), false);
}
}