Files
marcocosta10000 79ce2a1825 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
2026-10-17 21:46:23 -03:00

135 lines
4.0 KiB
PHP
Executable File

<?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();
}
}