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
+134
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Building;
|
||||
use App\Models\Character;
|
||||
use App\Models\Job;
|
||||
use Database\Seeders\CityWorldSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* World ticker behaviour: needs, salary, construction progress.
|
||||
*/
|
||||
class CityTickTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Seed world data.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(CityWorldSeeder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs decay on the first tick for awake characters.
|
||||
*/
|
||||
public function test_tick_decays_needs_once(): void
|
||||
{
|
||||
$character = Character::factory()->create([
|
||||
'state' => Character::STATE_AWAKE,
|
||||
'energy' => 100,
|
||||
'hunger' => 100,
|
||||
'thirst' => 100,
|
||||
'last_tick_at' => null,
|
||||
]);
|
||||
|
||||
$this->artisan('city:tick')->assertSuccessful();
|
||||
|
||||
$character->refresh();
|
||||
$this->assertLessThan(100, $character->hunger);
|
||||
$this->assertLessThan(100, $character->thirst);
|
||||
$this->assertNotNull($character->last_tick_at);
|
||||
|
||||
$hungerAfterFirst = $character->hunger;
|
||||
$this->artisan('city:tick')->assertSuccessful();
|
||||
$character->refresh();
|
||||
$this->assertSame($hungerAfterFirst, $character->hunger, 'Second immediate tick must be a no-op');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleeping restores energy and wakes the character at full energy.
|
||||
*/
|
||||
public function test_tick_recovers_energy_while_sleeping(): void
|
||||
{
|
||||
$character = Character::factory()->create([
|
||||
'state' => Character::STATE_SLEEPING,
|
||||
'energy' => 50,
|
||||
'last_tick_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->artisan('city:tick')->assertSuccessful();
|
||||
$character->refresh();
|
||||
$this->assertGreaterThan(50, $character->energy);
|
||||
|
||||
// Force another tick window.
|
||||
$character->forceFill(['last_tick_at' => now()->subMinute(), 'energy' => 95])->save();
|
||||
$this->artisan('city:tick')->assertSuccessful();
|
||||
$character->refresh();
|
||||
$this->assertSame(100, $character->energy);
|
||||
$this->assertSame(Character::STATE_AWAKE, $character->state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Working characters accrue salary each tick.
|
||||
*/
|
||||
public function test_tick_pays_salary_while_working(): void
|
||||
{
|
||||
$character = Character::factory()->create([
|
||||
'state' => Character::STATE_WORKING,
|
||||
'money' => 100,
|
||||
'last_tick_at' => now()->subMinute(),
|
||||
]);
|
||||
$character->job()->associate(Job::firstOrFail());
|
||||
$character->save();
|
||||
|
||||
$before = (float) $character->money;
|
||||
$this->artisan('city:tick')->assertSuccessful();
|
||||
$character->refresh();
|
||||
$this->assertGreaterThan($before, (float) $character->money);
|
||||
}
|
||||
|
||||
/**
|
||||
* Incomplete buildings advance and complete houses attach to the owner.
|
||||
*/
|
||||
public function test_tick_progresses_construction(): void
|
||||
{
|
||||
$character = Character::factory()->create();
|
||||
$building = Building::create([
|
||||
'city_id' => $character->city_id,
|
||||
'owner_character_id' => $character->id,
|
||||
'type' => Building::TYPE_HOUSE,
|
||||
'name' => 'Casa',
|
||||
'pos_x' => 20,
|
||||
'pos_z' => -70,
|
||||
'width' => 10,
|
||||
'depth' => 10,
|
||||
'height' => 5,
|
||||
'is_complete' => false,
|
||||
'build_progress' => 90,
|
||||
]);
|
||||
|
||||
$this->artisan('city:tick')->assertSuccessful();
|
||||
$building->refresh();
|
||||
$character->refresh();
|
||||
|
||||
$this->assertTrue($building->is_complete);
|
||||
$this->assertSame(100, $building->build_progress);
|
||||
$this->assertSame($building->id, $character->house_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduler registers the ticker every minute.
|
||||
*/
|
||||
public function test_scheduler_includes_city_tick(): void
|
||||
{
|
||||
$this->artisan('schedule:list')
|
||||
->expectsOutputToContain('city:tick')
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user