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
This commit is contained in:
Regular → Executable
Regular → Executable
@@ -0,0 +1,177 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user