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