From 457c364738be092e0cbadecb7bfb5e861ecf3cef Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Sun, 28 Jun 2026 15:34:01 +0300 Subject: [PATCH] docs: add nerve + gym training implementation plan Co-Authored-By: Claude Sonnet 4.6 --- .../superpowers/plans/2026-06-28-nerve-gym.md | 1158 +++++++++++++++++ 1 file changed, 1158 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-28-nerve-gym.md diff --git a/docs/superpowers/plans/2026-06-28-nerve-gym.md b/docs/superpowers/plans/2026-06-28-nerve-gym.md new file mode 100644 index 0000000..0159262 --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-nerve-gym.md @@ -0,0 +1,1158 @@ +# 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. \ No newline at end of file