# Nerve Stat + Gym Training Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a nerve resource that caps daily crime volume and a gym page where players spend energy to train the four combat stats. **Architecture:** Nerve mirrors the existing energy pattern — columns on `characters`, a `NerveService`, and two scheduled commands (trickle regen + midnight refill). Gym uses a new `GymController`, `TrainingService`, `GymSession` model, and a `gym_sessions` table with lazy daily-reset logic. **Tech Stack:** Laravel 11, Blade + Tailwind CSS (dark crime theme), Alpine.js, Eloquent ORM, artisan scheduler. ## Global Constraints - PHP 8.2+, Laravel 11.x - All views must use the dark crime theme: `bg-syndicate-*`, `text-crime-*`, `text-gold-*` classes - All new Blade views extend `layouts.game` and use `@section('game-content')` - Follow existing constructor-injection pattern in controllers (no `app()` or facades in constructors) - `$fillable` must be kept up to date on every model touched - No `$timestamps = false` on new models — use default timestamps - Artisan commands go in `app/Console/Commands/Game/` with signature prefix `game:` - Scheduler registration goes in `routes/console.php` --- ### Task 1: Database migrations **Files:** - Create: `database/migrations/2026_06_28_000001_add_nerve_to_characters_table.php` - Create: `database/migrations/2026_06_28_000002_add_nerve_cost_to_crimes_table.php` - Create: `database/migrations/2026_06_28_000003_add_nerve_cost_to_crime_attempts_table.php` - Create: `database/migrations/2026_06_28_000004_create_gym_sessions_table.php` **Interfaces:** - Produces: `characters.nerve` (int, default 100), `characters.max_nerve` (int, default 100), `characters.nerve_regen_at` (timestamp nullable), `crimes.nerve_cost` (int, default 1), `crime_attempts.nerve_cost` (int, default 1), `gym_sessions` table - [ ] **Step 1: Create the four migration files** `database/migrations/2026_06_28_000001_add_nerve_to_characters_table.php`: ```php integer('nerve')->default(100)->after('max_energy'); $table->integer('max_nerve')->default(100)->after('nerve'); $table->timestamp('nerve_regen_at')->nullable()->after('max_nerve'); }); } public function down(): void { Schema::table('characters', function (Blueprint $table) { $table->dropColumn(['nerve', 'max_nerve', 'nerve_regen_at']); }); } }; ``` `database/migrations/2026_06_28_000002_add_nerve_cost_to_crimes_table.php`: ```php integer('nerve_cost')->default(1)->after('energy_cost'); }); } public function down(): void { Schema::table('crimes', function (Blueprint $table) { $table->dropColumn('nerve_cost'); }); } }; ``` `database/migrations/2026_06_28_000003_add_nerve_cost_to_crime_attempts_table.php`: ```php integer('nerve_cost')->default(1)->after('experience_gained'); }); } public function down(): void { Schema::table('crime_attempts', function (Blueprint $table) { $table->dropColumn('nerve_cost'); }); } }; ``` `database/migrations/2026_06_28_000004_create_gym_sessions_table.php`: ```php id(); $table->foreignId('character_id')->constrained()->cascadeOnDelete(); $table->enum('stat', ['strength', 'defense', 'speed', 'dexterity']); $table->integer('sessions_today')->default(0); $table->date('reset_date'); $table->timestamps(); $table->unique(['character_id', 'stat']); }); } public function down(): void { Schema::dropIfExists('gym_sessions'); } }; ``` - [ ] **Step 2: Run migrations** ```bash php artisan migrate ``` Expected output: four migration lines saying "Migrating:" then "Migrated:" with no errors. - [ ] **Step 3: Commit** ```bash git add database/migrations/2026_06_28_000001_add_nerve_to_characters_table.php \ database/migrations/2026_06_28_000002_add_nerve_cost_to_crimes_table.php \ database/migrations/2026_06_28_000003_add_nerve_cost_to_crime_attempts_table.php \ database/migrations/2026_06_28_000004_create_gym_sessions_table.php git commit -m "feat: add nerve and gym_sessions migrations" ``` --- ### Task 2: Config + Character model nerve methods **Files:** - Modify: `config/game.php` - Modify: `app/Models/Character.php` - Modify: `app/Models/CrimeAttempt.php` - Modify: `app/Models/Crime.php` **Interfaces:** - Consumes: `characters.nerve`, `characters.max_nerve`, `characters.nerve_regen_at` (from Task 1) - Produces: - `config('game.nerve.start_amount')` → int - `config('game.nerve.max')` → int - `config('game.nerve.regen_rate')` → int - `config('game.nerve.regen_interval')` → int - `config('game.nerve.midnight_refill')` → bool - `config('game.gym.energy_cost')` → int - `config('game.gym.daily_cap')` → int - `config('game.gym.stat_gain')` → int - `config('game.energy.action_costs.gym')` → int - `Character::hasNerve(int $amount): bool` - `Character::consumeNerve(int $amount): bool` - `Character::addNerve(int $amount): void` - `Character::getNervePercentageAttribute(): float` - `Character::levelUp()` increments `max_nerve` by 2 - [ ] **Step 1: Add nerve + gym blocks to config/game.php** In `config/game.php`, add `'gym' => 20` to the `energy.action_costs` array: ```php 'action_costs' => [ 'crime' => 10, 'attack' => 15, 'travel' => 5, 'train' => 20, 'gym' => 20, ], ``` Then add two new top-level blocks after the `'health'` block: ```php // Nerve system configuration 'nerve' => [ 'start_amount' => 100, 'max' => 100, 'regen_rate' => 1, // nerve per tick 'regen_interval' => 300, // seconds (5 minutes) 'midnight_refill' => true, ], // Gym training configuration 'gym' => [ 'energy_cost' => 20, 'daily_cap' => 5, // sessions per stat per day 'stat_gain' => 1, // stat points per session ], ``` - [ ] **Step 2: Add nerve fields and methods to Character model** In `app/Models/Character.php`, add to `$fillable` array (after `'max_energy'`): ```php 'nerve', 'max_nerve', 'nerve_regen_at', ``` Add to `$casts` array: ```php 'nerve_regen_at' => 'datetime', ``` Add to the `levelUp()` method, after `$this->max_energy += 5;`: ```php $this->max_nerve += 2; ``` Add these four methods after the `addEnergy()` method: ```php public function hasNerve(int $amount): bool { return $this->nerve >= $amount; } public function consumeNerve(int $amount): bool { if (!$this->hasNerve($amount)) { return false; } $this->nerve -= $amount; $this->save(); return true; } public function addNerve(int $amount): void { $this->nerve = min($this->max_nerve, $this->nerve + $amount); $this->save(); } public function getNervePercentageAttribute(): float { if ($this->max_nerve <= 0) { return 0; } return min(100, ($this->nerve / $this->max_nerve) * 100); } ``` - [ ] **Step 3: Add nerve_cost to CrimeAttempt model** In `app/Models/CrimeAttempt.php`, add `'nerve_cost'` to `$fillable`: ```php protected $fillable = [ 'character_id', 'crime_id', 'success', 'reward', 'experience_gained', 'nerve_cost', 'attempted_at', ]; ``` - [ ] **Step 4: Add nerve_cost to Crime model** In `app/Models/Crime.php`, add `'nerve_cost'` to `$fillable`: ```php protected $fillable = [ 'name', 'slug', 'description', 'difficulty', 'success_rate', 'reward_min', 'reward_max', 'experience', 'energy_cost', 'nerve_cost', 'cooldown', 'requires_level', 'location_id', ]; ``` Also add `@property int $nerve_cost` to the docblock at the top of `Crime.php`: ```php * @property int $nerve_cost ``` - [ ] **Step 5: Verify with tinker** ```bash php artisan tinker ``` Then run: ```php $c = App\Models\Character::first(); $c->nerve; // should return 100 $c->max_nerve; // should return 100 $c->hasNerve(1); // should return true $c->consumeNerve(1); // should return true $c->nerve; // should return 99 $c->addNerve(10); $c->nerve; // should return 100 (capped at max) $c->nerve_percentage; // should return 100.0 exit; ``` - [ ] **Step 6: Commit** ```bash git add config/game.php app/Models/Character.php app/Models/CrimeAttempt.php app/Models/Crime.php git commit -m "feat: add nerve config and Character nerve methods" ``` --- ### Task 3: NerveService + nerve regen commands **Files:** - Create: `app/Services/Game/NerveService.php` - Create: `app/Console/Commands/Game/ProcessNerveRegen.php` - Create: `app/Console/Commands/Game/NerveMidnightRefill.php` - Modify: `routes/console.php` **Interfaces:** - Consumes: `Character::addNerve()`, `Character::hasNerve()`, `Character::consumeNerve()`, `config('game.nerve.*')` (from Task 2) - Produces: - `NerveService::consume(Character $character, int $amount): bool` - `NerveService::regenerate(Character $character): void` - `NerveService::refillAll(): int` (returns count of characters updated) - Artisan command `game:nerve-regen` - Artisan command `game:nerve-midnight-refill` - Scheduler registrations for both commands - [ ] **Step 1: Create NerveService** `app/Services/Game/NerveService.php`: ```php canAct()) { return false; } return $character->consumeNerve($amount); } public function regenerate(Character $character): void { $regenRate = config('game.nerve.regen_rate', 1); if ($character->max_nerve <= 0) { $character->max_nerve = config('game.nerve.max', 100); } $character->addNerve($regenRate); } public function refillAll(): int { return Character::where('nerve', '<', \DB::raw('max_nerve')) ->where('status', 'active') ->update(['nerve' => \DB::raw('max_nerve')]); } } ``` - [ ] **Step 2: Create ProcessNerveRegen command** `app/Console/Commands/Game/ProcessNerveRegen.php`: ```php info('Starting nerve regeneration...'); $characters = Character::where('nerve', '<', \DB::raw('max_nerve')) ->where('status', 'active') ->get(); $count = $characters->count(); $this->info("Regenerating nerve for {$count} characters..."); foreach ($characters as $character) { $nerveService->regenerate($character); } $this->info('Nerve regeneration completed!'); } } ``` - [ ] **Step 3: Create NerveMidnightRefill command** `app/Console/Commands/Game/NerveMidnightRefill.php`: ```php info('Running midnight nerve refill...'); $count = $nerveService->refillAll(); $this->info("Nerve refilled for {$count} characters."); } } ``` - [ ] **Step 4: Register commands in scheduler** In `routes/console.php`, add after the existing `Artisan::command('inspire', ...)` block: ```php use Illuminate\Support\Facades\Schedule; Schedule::command('game:nerve-regen')->everyFiveMinutes(); Schedule::command('game:nerve-midnight-refill')->dailyAt('00:00'); ``` - [ ] **Step 5: Smoke-test both commands** ```bash php artisan game:nerve-regen ``` Expected output: ``` Starting nerve regeneration... Regenerating nerve for 0 characters... Nerve regeneration completed! ``` (0 because all characters are already at max nerve after migration.) ```bash php artisan game:nerve-midnight-refill ``` Expected output: ``` Running midnight nerve refill... Nerve refilled for 0 characters. ``` - [ ] **Step 6: Commit** ```bash git add app/Services/Game/NerveService.php \ app/Console/Commands/Game/ProcessNerveRegen.php \ app/Console/Commands/Game/NerveMidnightRefill.php \ routes/console.php git commit -m "feat: add NerveService and nerve regen scheduled commands" ``` --- ### Task 4: Wire nerve into crime attempts **Files:** - Modify: `app/Services/Game/CrimeService.php` - Modify: `app/Models/Crime.php` (`canBeAttemptedBy`) **Interfaces:** - Consumes: `NerveService::consume()` (Task 3), `Character::hasNerve()` (Task 2), `Crime::nerve_cost` (Task 2) - Produces: crimes check nerve before proceeding; `nerve_cost` recorded on `CrimeAttempt` - [ ] **Step 1: Add nerve check to Crime::canBeAttemptedBy** In `app/Models/Crime.php`, update `canBeAttemptedBy()` to also check nerve: ```php public function canBeAttemptedBy(Character $character): bool { if ($this->requires_level && $character->level < $this->requires_level) { return false; } if ($character->energy < $this->energy_cost) { return false; } if ($character->nerve < $this->nerve_cost) { return false; } if ($this->location_id && $character->location_id !== $this->location_id) { return false; } return true; } ``` - [ ] **Step 2: Update CrimeService to consume nerve and record nerve_cost** In `app/Services/Game/CrimeService.php`: Add `use App\Services\Game\NerveService;` to the imports. Add `protected NerveService $nerveService;` as a class property. Update the constructor to instantiate it: ```php public function __construct() { $this->energyService = new EnergyService(); $this->economyService = new EconomyService(); $this->nerveService = new NerveService(); } ``` In `attemptCrime()`, add nerve consumption after the existing energy consumption block: ```php // Consume nerve if (!$this->nerveService->consume($character, $crime->nerve_cost)) { throw new \Exception('Not enough nerve to attempt this crime.'); } ``` Update the `CrimeAttempt::create()` call to include `nerve_cost`: ```php $attempt = CrimeAttempt::create([ 'character_id' => $character->id, 'crime_id' => $crime->id, 'success' => $isSuccess, 'reward' => $reward, 'experience_gained' => $experience, 'nerve_cost' => $crime->nerve_cost, 'attempted_at' => now(), ]); ``` - [ ] **Step 3: Manual test — attempt a crime and confirm nerve decrements** ```bash php artisan tinker ``` ```php $character = App\Models\Character::first(); $character->nerve; // note the starting value (e.g. 100) $crime = App\Models\Crime::first(); $service = new App\Services\Game\CrimeService(); $attempt = $service->attemptCrime($character, $crime); $attempt->nerve_cost; // should be 1 $character->fresh()->nerve; // should be 99 exit; ``` - [ ] **Step 4: Commit** ```bash git add app/Services/Game/CrimeService.php app/Models/Crime.php git commit -m "feat: wire nerve check and deduction into crime attempts" ``` --- ### Task 5: GymSession model + TrainingService **Files:** - Create: `app/Models/GymSession.php` - Create: `app/Services/Game/TrainingService.php` **Interfaces:** - Consumes: `Character::consumeEnergy()`, `config('game.gym.*')` (Task 2) - Produces: - `TrainingService::train(Character $character, string $stat): array` — returns `['success' => bool, 'message' => string, 'stat_value' => int]` - `TrainingService::getSessionsToday(Character $character, string $stat): int` - `TrainingService::getSessionsRemaining(Character $character, string $stat): int` - `GymSession` Eloquent model for `gym_sessions` table - [ ] **Step 1: Create GymSession model** `app/Models/GymSession.php`: ```php 'date', ]; public function character(): BelongsTo { return $this->belongsTo(Character::class); } } ``` - [ ] **Step 2: Create TrainingService** `app/Services/Game/TrainingService.php`: ```php false, 'message' => 'Invalid stat.', 'stat_value' => 0]; } $energyCost = config('game.gym.energy_cost', 20); $dailyCap = config('game.gym.daily_cap', 5); $statGain = config('game.gym.stat_gain', 1); if (!$character->hasEnergy($energyCost)) { return [ 'success' => false, 'message' => "Not enough energy ({$energyCost} required).", 'stat_value' => $character->$stat, ]; } $sessionsToday = $this->getSessionsToday($character, $stat); if ($sessionsToday >= $dailyCap) { return [ 'success' => false, 'message' => "Daily training cap reached for {$stat} ({$dailyCap} sessions).", 'stat_value' => $character->$stat, ]; } // Deduct energy $character->consumeEnergy($energyCost); // Increment stat $character->$stat += $statGain; $character->save(); // Upsert gym_sessions row $session = GymSession::firstOrNew([ 'character_id' => $character->id, 'stat' => $stat, ]); if (!$session->reset_date || $session->reset_date->lt(today())) { $session->sessions_today = 0; $session->reset_date = today(); } $session->sessions_today += 1; $session->save(); return [ 'success' => true, 'message' => ucfirst($stat) . " increased to {$character->$stat}!", 'stat_value' => $character->$stat, ]; } public function getSessionsToday(Character $character, string $stat): int { $session = GymSession::where('character_id', $character->id) ->where('stat', $stat) ->first(); if (!$session || $session->reset_date->lt(today())) { return 0; } return $session->sessions_today; } public function getSessionsRemaining(Character $character, string $stat): int { $cap = config('game.gym.daily_cap', 5); return max(0, $cap - $this->getSessionsToday($character, $stat)); } } ``` - [ ] **Step 3: Test TrainingService via tinker** ```bash php artisan tinker ``` ```php $character = App\Models\Character::first(); $character->strength; // note starting value $service = new App\Services\Game\TrainingService(); // Train strength $result = $service->train($character, 'strength'); $result['success']; // true $result['stat_value']; // original + 1 $character->fresh()->strength; // should match stat_value $character->fresh()->energy; // should be 20 less // Check session count $service->getSessionsToday($character->fresh(), 'strength'); // 1 $service->getSessionsRemaining($character->fresh(), 'strength'); // 4 // Train 4 more times to hit cap $service->train($character->fresh(), 'strength'); $service->train($character->fresh(), 'strength'); $service->train($character->fresh(), 'strength'); $result = $service->train($character->fresh(), 'strength'); $result['success']; // true (5th session) $result = $service->train($character->fresh(), 'strength'); $result['success']; // false — capped $result['message']; // "Daily training cap reached..." exit; ``` - [ ] **Step 4: Commit** ```bash git add app/Models/GymSession.php app/Services/Game/TrainingService.php git commit -m "feat: add GymSession model and TrainingService" ``` --- ### Task 6: GymController + routes **Files:** - Create: `app/Http/Controllers/Game/GymController.php` - Modify: `routes/game.php` **Interfaces:** - Consumes: `TrainingService::train()`, `TrainingService::getSessionsToday()`, `TrainingService::getSessionsRemaining()` (Task 5); `config('game.gym.*')` (Task 2) - Produces: - `GET /game/gym` → `game.gym.index` view with `$character`, `$stats`, `$energyCost`, `$dailyCap` - `POST /game/gym/train` → redirects to `game.gym.index` with flash - [ ] **Step 1: Create GymController** `app/Http/Controllers/Game/GymController.php`: ```php trainingService = new TrainingService(); } public function index(Request $request): View { $character = $request->user()->character; $dailyCap = config('game.gym.daily_cap', 5); $energyCost = config('game.gym.energy_cost', 20); $stats = []; foreach (['strength', 'defense', 'speed', 'dexterity'] as $stat) { $stats[$stat] = [ 'value' => $character->$stat, 'sessions_today' => $this->trainingService->getSessionsToday($character, $stat), 'sessions_remaining'=> $this->trainingService->getSessionsRemaining($character, $stat), ]; } return view('game.gym.index', compact('character', 'stats', 'energyCost', 'dailyCap')); } public function train(Request $request): RedirectResponse { $request->validate(['stat' => 'required|in:strength,defense,speed,dexterity']); $character = $request->user()->character; $result = $this->trainingService->train($character, $request->stat); if ($result['success']) { return redirect()->route('game.gym.index')->with('success', $result['message']); } return redirect()->route('game.gym.index')->with('error', $result['message']); } } ``` - [ ] **Step 2: Add gym routes to routes/game.php** In `routes/game.php`, add inside the `Route::middleware(['auth', 'verify.character'])` group, after the inventory routes block: ```php // Gym routes Route::prefix('game/gym')->name('game.gym.')->group(function () { Route::get('/', [GymController::class, 'index'])->name('index'); Route::post('/train', [GymController::class, 'train'])->name('train')->middleware(['check.energy:gym']); }); ``` Also add the import at the top of the file: ```php use App\Http\Controllers\Game\GymController; ``` - [ ] **Step 3: Verify routes are registered** ```bash php artisan route:list --name=game.gym ``` Expected output showing two routes: ``` GET|HEAD game/gym game.gym.index GymController@index POST game/gym/train game.gym.train GymController@train ``` - [ ] **Step 4: Commit** ```bash git add app/Http/Controllers/Game/GymController.php routes/game.php git commit -m "feat: add GymController and gym routes" ``` --- ### Task 7: Gym view (dark crime theme) **Files:** - Create: `resources/views/game/gym/index.blade.php` **Interfaces:** - Consumes: `$character`, `$stats` (array keyed by stat name with `value`, `sessions_today`, `sessions_remaining`), `$energyCost`, `$dailyCap` (from Task 6) - Produces: rendered gym page at `/game/gym` - [ ] **Step 1: Create the gym view** `resources/views/game/gym/index.blade.php`: ```blade @extends('layouts.game') @section('title', 'Gym') @section('game-content')
{{-- Header --}}

Gym

Train your stats. Each session costs {{ $energyCost }} energy. Max {{ $dailyCap }} sessions per stat per day.

{{-- Energy warning --}} @if($character->energy < $energyCost)
You need at least {{ $energyCost }} energy to train. Current energy: {{ $character->energy }}/{{ $character->max_energy }}.
@endif {{-- Stat cards --}}
@foreach($stats as $stat => $data) @php $icons = ['strength' => '💪', 'defense' => '🛡️', 'speed' => '⚡', 'dexterity' => '🎯']; $labels = ['strength' => 'Strength', 'defense' => 'Defense', 'speed' => 'Speed', 'dexterity' => 'Dexterity']; $canTrain = $character->energy >= $energyCost && $data['sessions_remaining'] > 0; @endphp
{{ $icons[$stat] }}

{{ $labels[$stat] }}

Current: {{ $data['value'] }}

Sessions today

{{ $data['sessions_today'] }} / {{ $dailyCap }}

{{-- Progress bar for daily cap --}}
@csrf
@endforeach
@endsection ``` - [ ] **Step 2: Load the gym page in a browser and verify it renders** Start the dev server if not running: ```bash php artisan serve ``` Visit `http://localhost:8000/game/gym`. Confirm: - Four stat cards render (Strength, Defense, Speed, Dexterity) - Each shows current stat value and 0/5 sessions today - Train buttons are active (green) when energy ≥ 20 - Page uses dark crime theme consistent with other game pages - [ ] **Step 3: Test train action in browser** Click "Train (+1) — 20 energy" on Strength. Confirm: - Page redirects back to `/game/gym` - Success flash message appears: "Strength increased to X!" - Strength card now shows updated value and 1/5 sessions - Energy in the top bar decreased by 20 - [ ] **Step 4: Commit** ```bash git add resources/views/game/gym/index.blade.php git commit -m "feat: add gym page view" ``` --- ### Task 8: Nerve bar in stats bar + nav link + dashboard **Files:** - Modify: `resources/views/game/partials/stats-bar.blade.php` - Modify: `resources/views/layouts/navigation.blade.php` - Modify: `resources/views/game/dashboard.blade.php` **Interfaces:** - Consumes: `$character->nerve`, `$character->max_nerve` (Task 2); `route('game.gym.index')` (Task 6) - Produces: nerve displayed in top stats bar; Gym link in sidebar; nerve bar card on dashboard - [ ] **Step 1: Add nerve to stats-bar partial** In `resources/views/game/partials/stats-bar.blade.php`, add a nerve chip after the Energy chip (after the closing `` of the Energy block): ```blade {{-- Nerve --}}
🧠 {{ $character->nerve }}/{{ $character->max_nerve }}
``` - [ ] **Step 2: Add Gym link to sidebar navigation** In `resources/views/layouts/navigation.blade.php`, add after the Inventory `
  • ` block and before the Profile `
  • ` block: ```blade
  • 💪 Gym
  • ``` - [ ] **Step 3: Add nerve card to dashboard** In `resources/views/game/dashboard.blade.php`, add a new card after the "Quick Stats" card. Insert it as the third item in the grid, pushing Finances to position 4 (the grid handles reflow automatically): ```blade {{-- Nerve --}}

    Nerve

    Available {{ $character->nerve }} / {{ $character->max_nerve }}

    Nerve gates crime volume. Recharges over time and fully refills at midnight.

    Go to Gym
    ``` - [ ] **Step 4: Verify in browser** Visit `http://localhost:8000/game`. Confirm: - Nerve chip (`🧠 100/100`) appears in the top stats bar between Energy and Cash - Gym link (`💪 Gym`) appears in the sidebar and highlights when on `/game/gym` - Dashboard shows a Nerve card with a purple progress bar and "Go to Gym" link - [ ] **Step 5: Commit** ```bash git add resources/views/game/partials/stats-bar.blade.php \ resources/views/layouts/navigation.blade.php \ resources/views/game/dashboard.blade.php git commit -m "feat: add nerve to stats bar, gym nav link, nerve dashboard card" ``` --- ## Self-Review **Spec coverage check:** | Spec requirement | Task | |-----------------|------| | nerve/max_nerve/nerve_regen_at columns on characters | Task 1 | | nerve_cost on crimes table (default 1) | Task 1 | | nerve_cost on crime_attempts table | Task 1 | | gym_sessions table with unique(character_id, stat) | Task 1 | | config/game.php nerve + gym blocks | Task 2 | | config gym.energy_cost added to energy.action_costs | Task 2 | | Character::hasNerve, consumeNerve, addNerve, nerve_percentage | Task 2 | | CrimeAttempt $fillable includes nerve_cost | Task 2 | | Crime $fillable includes nerve_cost | Task 2 | | levelUp() increments max_nerve by 2 | Task 2 | | NerveService::consume, regenerate, refillAll | Task 3 | | game:nerve-regen artisan command | Task 3 | | game:nerve-midnight-refill artisan command | Task 3 | | Scheduler registrations | Task 3 | | Crime::canBeAttemptedBy checks nerve | Task 4 | | CrimeService consumes nerve + records nerve_cost | Task 4 | | GymSession model | Task 5 | | TrainingService::train with lazy daily reset | Task 5 | | TrainingService::getSessionsToday + getSessionsRemaining | Task 5 | | GymController::index + train | Task 6 | | GET /game/gym + POST /game/gym/train routes | Task 6 | | check.energy:gym middleware on train route | Task 6 | | Gym view (dark theme, 4 stat cards, disabled states) | Task 7 | | Nerve chip in stats bar | Task 8 | | Gym link in sidebar nav | Task 8 | | Nerve card on dashboard | Task 8 | All spec requirements covered. No placeholders found. Types and method names are consistent across tasks.