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); } /** * A vehicle collision admits the character for one game hour at the hospital. */ public function test_accident_admits_character_for_one_game_hour(): void { $response = $this->postJson('/city/actions/accident'); $response->assertOk() ->assertJsonPath('character.state', Character::STATE_HOSPITAL) ->assertJsonStructure(['message', 'hospitalized_until', 'bed' => ['pos_x', 'pos_z']]); $this->character->refresh(); $this->assertSame(Character::STATE_HOSPITAL, $this->character->state); $this->assertNotNull($this->character->hospitalized_until); $this->assertEqualsWithDelta( now()->addSeconds(Character::HOSPITAL_STAY_SECONDS)->getTimestamp(), $this->character->hospitalized_until->getTimestamp(), 5, ); $this->assertSame(Character::HOSPITAL_STAY_SECONDS, 10, 'One game hour = 240s / 24'); $hospital = Building::where('type', Building::TYPE_HOSPITAL)->firstOrFail(); $this->assertSame((float) $hospital->pos_x, (float) $this->character->pos_x); } /** * While admitted, ordinary actions are rejected. */ public function test_actions_blocked_while_hospitalized(): void { $this->character->admitToHospital(); $this->postJson('/city/actions/eat')->assertStatus(422); $this->postJson('/city/actions/work')->assertStatus(422); $this->postJson('/city/actions/build', [ 'pos_x' => 10, 'pos_z' => 50, ])->assertStatus(422); } /** * Position updates keep the character in the hospital bed while admitted. */ public function test_position_frozen_while_hospitalized(): void { $bed = $this->character->admitToHospital(); $this->postJson('/city/actions/position', ['pos_x' => 10, 'pos_z' => 60]) ->assertOk() ->assertJsonPath('state', Character::STATE_HOSPITAL); $this->character->refresh(); $this->assertSame($bed[0], (float) $this->character->pos_x); $this->assertSame($bed[1], (float) $this->character->pos_z); } /** * After the one-hour stay the ticker releases the character. */ public function test_ticker_releases_after_one_game_hour(): void { $this->character->admitToHospital(); // Move the stay into the past to simulate elapsed time. $this->character->forceFill([ 'hospitalized_until' => now()->subSecond(), 'last_tick_at' => null, ])->save(); $this->artisan('city:tick')->assertSuccessful(); $this->character->refresh(); $this->assertSame(Character::STATE_AWAKE, $this->character->state); $this->assertNull($this->character->hospitalized_until); } /** * While still within the stay the ticker keeps the hospital state. */ public function test_ticker_keeps_admission_before_discharge(): void { $this->character->admitToHospital(); $this->character->forceFill(['last_tick_at' => null])->save(); $this->artisan('city:tick')->assertSuccessful(); $this->character->refresh(); $this->assertSame(Character::STATE_HOSPITAL, $this->character->state); $this->assertTrue($this->character->hospitalized_until->isFuture()); } /** * City data payload exposes the discharge timestamp. */ public function test_city_data_includes_hospital_state(): void { $this->character->admitToHospital(); $this->getJson('/city/data') ->assertOk() ->assertJsonPath('me.state', Character::STATE_HOSPITAL) ->assertJsonPath('me.hospitalized_until', $this->character->hospitalized_until->toIso8601String()); } /** * Hospital building exists in the seeded city. */ public function test_hospital_building_seeded(): void { $city = City::where('slug', 'city10')->firstOrFail(); $hospital = Building::where('city_id', $city->id) ->where('type', Building::TYPE_HOSPITAL) ->first(); $this->assertNotNull($hospital); $this->assertTrue($hospital->is_complete); } }